Delphi Cookbook - Second Edition

4.5 (19 reviews total)
By Daniele Teti
  • Instant online access to over 7,500+ books and videos
  • Constantly updated with 100+ new titles each month
  • Breadth and depth in over 1,000+ technologies
  1. Delphi Basics

About this book

Delphi is a cross-platform Integrated Development Environment (IDE) that supports rapid application development for Microsoft Windows, Apple Mac OS X, Google Android, and Apple iOS. It helps you to concentrate on the real business and save yourself the pain of wandering amid GUI widget details, or having to tackle inter-platform incompatibilities. It also has a wide range of drag-and-drop controls, helping you code your business logic into your business model, and it compiles natively for desktop and mobile platforms.

This book will teach you how to design and develop applications, deploy them on the cloud platform, and distribute them within an organization via Google Play and other similar platforms.

You will begin with the basics of Delphi and get acquainted with JSON format strings, XSLT transformations, unicode encodings and various types of streams. We then move on to more advanced topics such as developing higher-order functions and using enumerators and RTTI. You will get an understanding of how Delphi RTL functions and how to use FireMonkey in a VCL application. We will then cover topics such as multithreading, using the parallel programming library and putting Delphi on a server. We will also take a look at the new feature of WebBroker Apache modules and then ride the mobile revolution with FireMonkey.

By the end of the book, you will be able to develop and deploy cross-platform applications using Delphi.

Publication date:
June 2016
Publisher
Packt
Pages
470
ISBN
9781785287428

 

Chapter 1. Delphi Basics

In this chapter, we will cover the following topics:

  • Changing your application's look and feel with the VCL style and without any code

  • Changing the style of your application at runtime

  • Customizing TDBGrid

  • Using owner draw combos and listboxes

  • Making an owner draw control aware of the VCL styles

  • Creating a stack of embedded forms

  • Manipulating JSON

  • Manipulating and transforming XML documents

  • I/O in the 21st century – knowing the streams

  • Creating a Windows service

  • Associating a file extension with your application on Windows

  • Being coherent with the Windows look and feel using TTaskDialog

 

Introduction


This chapter will explain some of the day-to-day needs of a Delphi programmer. These are ready-to-use recipes that will be useful every day and have been selected ahead of a lot of others because, although they may be obvious for some experienced users, they are still very useful. Even if there isn't specifically database-related code, many of the recipes can also be used (or sometimes, especially used) when you are dealing with data.

 

Changing your application look and feel with VCL styles and no code


Visual Component Library (VCL) styles are a major new entry in the latest versions of Delphi. They have been introduced in Delphi XE2 and are still one of the lesser known features for the good old Delphi developers. However, as usual, some businessmen say "looks matter" so the look and feel of your application could be one of the reasons to choose one product over another from a competitor. Consider that with a few mouse clicks, you can apply many different styles to your application to change the look and feel of your applications. So, why not to give it a try?

Getting ready

VCL styles can be used to revamp an old application or to create a new one with a non-standard GUI. VCL styles are a completely different beast to FireMonkey styles. They are both styles, but with completely different approaches and behavior.

To get started with VCL styles, we'll use a new application. So, let's create a new VCL application and drag and drop some components onto the main form (for example, two TButton, one TListBox, one TComboBox, and a couple of TCheckBox).

You can now see the resultant form that is running on my Windows 8.1 machine:

Figure 1.1: A form without style

How to do it…

Now, we've got to apply a set of nice styles by following these steps:

  1. Go to Project | Options from the menu. Then, in the resultant dialog, go to Application | Appearance and select all the styles that we want to include in our application.

  2. Using the Preview button, the IDE shows a simple demo form with some controls, and we can get an idea about the final result of our styled form. Feel free to experiment and choose the style—or set of styles—that you like. Only one style at a time will be used, but we can link the necessary resources into the executable and select the proper one at runtime.

  3. After selecting all the required styles from the list, we've got to select one in the combo box at the bottom. This style will be the default style for our form and will be loaded as soon as the application starts. You can delay this choice and make it at runtime using code if you prefer.

  4. Click on OK, hit F9 (or go to Run | Run), and your application is styled:

    Figure 1.2: The same form as Figure 1.1 but with the Iceberg Classico style applied

How it works…

Selecting one or more styles from Project | Options | Application | Appearance will cause the Delphi linker to link the style resource into your executable. It is possible to link many styles into your executable, but you can use only one style at a time. So, how does Delphi know which style you want to use when there are more than one? If you check the Project file (the file with the .dpr extension) by going to Project | View Source Menu, you can see where and how this little magic happens.

The following lines are the interesting section:

begin
  Application.Initialize;
  Application.MainFormOnTaskbar := True;
  
TStyleManager.TrySetStyle('Iceberg Classico');
  Application.CreateForm(TMainForm, MainForm);
  Application.Run;
end

When we've selected the Iceberg Classico style as the default style, the Delphi IDE added a line just before the creation of the main form, setting the default style for all the applications using TStyleManager.TrySetStyle static methods.

TStyleManager is very important class when dealing with VCL styles. We'll see more about it in the upcoming recipe, where you'll learn how to change styles at runtime.

There's more…

Delphi and C++ Builder 10.1 Berlin come with 36 VCL styles available in the folder (with a standard installation):

C:\Program Files (x86)\Embarcadero\Studio\18.0\Redist\styles\vcl\

Moreover, it is possible to create your own styles or modify the existing ones using the Bitmap Style Designer. You can access it by going to Tools | Bitmap Style Designer Menu.

For more details on how to create or customize a VCL style, visit http://docwiki.embarcadero.com/RADStudio/en/Creating_a_Style_using_the_Bitmap_Style_Designer.

The Bitmap Style Designer also provides test applications to test VCL styles.

 

Changing the style of your VCL application at runtime


VCL styles are a powerful way to change the appearance of your application. One of the main features of VCL styles is the ability to change the style while the application is running.

Getting ready

Because a VCL Style is simply a particular kind of binary file, we can allow our users to load their preferred styles at runtime. We could even provide new styles by publishing them on a website or sending them by e-mail to our customers.

In this recipe, we'll change the style while the application is running using a style already linked at design time, or let the user choose between a set of styles deployed inside a folder.

How to do it…

Style manipulation at runtime is done using the class methods of the TStyleManager class. Follow these steps to change the style of your VCL application at runtime:

  1. Create a brand new VCL application and add the Vcl.Themes and Vcl.Styles units to the implementation main form uses section. These units are required to use VCL styles at runtime.

  2. Drop on the form a TListBox, two TButton, and a TOpenDialog. Leave the default component names.

  3. Go to Project | Appearance and select eight styles of your choice from the list. Leave the Default style to Windows.

  4. The TStyleManager.StyleNames property contains names of all the available styles. In the FormCreate event handler, we have to load the already linked styles present in the executable into the listbox to let the user choose one of them. So, create a new procedure called StylesListRefresh with the following code and call it from the FormCreate event handler:

    procedure TMainForm.StylesListRefresh;
    var
      stylename: string;
     
    begin
      ListBox1.Clear;
      // retrieve all the styles linked in the executable
      for stylename in TStyleManager.StyleNames do
      begin
        ListBox1.Items.Add(stylename);
      end;
    end;
  5. In the Button1Click event handler, we've to set the current style according to the one selected from the ListBox1 using the code as follows:

    TStyleManager.SetStyle(ListBox1.Items[ListBox1.ItemIndex]);
  6. The Button2Click event handler should allow the user to select a style from the disk. So, we have to create a folder named styles at the level of our executable and copy a .vsf file from the default style directory, which, in RAD Studio 10.1 Berlin, is C:\Program Files (x86)\Embarcadero\Studio\18.0\Redist\styles\vcl\.

  7. After copying, write the following code under the Button2Click event handler. This code allows the user to choose a style file directly from the disk. Then, you can select one of the loaded styles from the listbox and click on Button1 to apply it to application:

    if OpenDialog1.Execute then
    begin
      if TStyleManager.IsValidStyle(OpenDialog1.FileName) then
      begin
        //load the style file
      TStyleManager.LoadFromFile(OpenDialog1.FileName);
      //refresh the list with the currently available styles
        StylesListRefresh;
        ShowMessage('New VCL Style has been loaded');
      end
      else
        ShowMessage('The file is not a valid VCL Style!');
      end;
    end;
  8. Just to have an idea of how the different controls appear with the selected style, drag and drop some controls on the right-hand side of the form. The following image shows my application with some styles loaded, some at design time and some from the disk.

  9. Hit F9 (or go to Run | Run), and play with your application using and loading styles from the disk:

    Figure 2.1: The Style Chooser form with a Torquoise Gray style loaded

How it works…

The TStyleManager class has all the methods we need to:

  • Inspect the loaded styles with TStyleManager.StyleNames

  • Apply an already loaded style to the running application using the following code:

    TStyleManager.SetStyle('StyleName')
  • Check whether a file is a valid style using the following code:

    TStyleManager.IsValidStyle('StylePathFileName')
  • Load a style file from the disk using the following code:

    TStyleManager.LoadFromFile('StylePathFileName')

After loading new styles from the disk, the new styles are completely similar to the styles linked in the executable during the compile and link phases and can be used in the same way.

There's more…

Other things to consider are third-party controls. If your application uses third-party controls, take care with their style support (some third-party controls are not be style aware). If your external components do not support styles, you will end up with some styled controls (the original included in Delphi) and some not styled (your external third-party controls)!

Go to Tools | Bitmap Style Designer. Using a custom VCL style we can also:

  • Change application colors, such as ButtonNormal, ButtonPressed, ButtonFocused, ButtonHot, and others

  • Override system colors, such as clCaptionText, clBtnFace, clActiveCaption, and so on

  • Font color and font name for particular controls familiar to ButtonTextNormal, ButtonTextPressed, ButtonTextFocused, ButtonTextHot, and many others

    Figure 2.2: The Bitmap Style Designer while it is working on a custom style

 

Customizing TDBGrid


The adage "A picture is worth a thousand words" refers to the notion that a complex idea can be conveyed with just a single still image. Sometimes, even a simple concept is easier to understand and nicer to see if it is represented by images. In this recipe, we'll see how to customize TDBGrid to visualize a graphical representation of data.

Getting ready

Many VCL controls are able to delegate their drawing, or part of it, to user code. It means that we can use simple event handlers to draw standard components in different ways. It is not always simple, but TDBGrid is customizable in a really easy way. Let's say that we have a class of musicians that have to pass a set of exams. We want to show the percent of exams already passed with a progress bar and, if the percent is higher than 50, there should also be a check in another column. Moreover, after listening to the pieces played at the exams, each musician received votes from an external examination committee. The last column needs to show the mean of votes from this committee as a rating from 0 to 5.

How to do it…

We'll use a special in memory table from the FireDAC library. FireDAC is a new data access library from Embarcadero included in RAD Studio since version XE5. If some of the code seems unclear at the moment, consider the in-memory table as a normal TDataSet descendant, which holds its data only in memory. However, at the end of the section, there are some links to the FireDAC documentation, and I strongly suggest that you read them if you still don't know FireDAC:

  1. Create a brand new VCL application and drop a TFDMemTable, a TDBGrid, a TDataSource, and a TDBNavigator on the form. Connect all the components in the usual way (TDBGrid connected to TDataSource followed by TFDMemTable). Set TDBGrid's font size to 18. This will create more space in the cell for our graphical representation.

  2. Using the TFDMemTable fields editor, add the following fields and then activate the dataset by setting its Active property to True:

    Field name

    Field data type

    Field type

    FullName

    String (size 50)

    Data

    TotalExams

    Integer

    Data

    PassedExams

    Integer

    Data

    Rating

    Float

    Data

    PercPassedExams

    Float

    Calculated

    MoreThan50Percent

    Boolean

    Calculated

  3. Now, add all the columns to TDBGrid by right-clicking and selecting Columns Editor. Then, again right-click and select Add all fields on the resultant window. Then, rearrange the columns as shown here and give a nice title caption:

    • FullName

    • TotalExams

    • PassedExams

    • PercPassedExams

    • MoreThan50Percent

    • Rating

  4. In a real application, we should load real data from some sort of database. However, for now, we'll use some custom data generated in code. We have to load this data into the dataset with the code as follows:

    procedure TMainForm.FormCreate(Sender: TObject);
    begin
      FDMemTable1.AppendRecord(
    ['Ludwig van Beethoven', 30, 10, 4]);
      FDMemTable1.AppendRecord(
    ['Johann Sebastian Bach', 24, 10, 2.5]);
      FDMemTable1.AppendRecord(
    ['Wolfgang Amadeus Mozart', 30, 30, 5]);
      FDMemTable1.AppendRecord(
    ['Giacomo Puccini', 25, 10, 2.2]);
      FDMemTable1.AppendRecord(
    ['Antonio Vivaldi', 20, 20, 4.7]);
      FDMemTable1.AppendRecord(
    ['Giuseppe Verdi', 30, 5, 5]);
      FDMemTable1.AppendRecord(
    ['John Doe', 24, 5, 1.2]); 
    end;
  5. Do you remember? We've two calculated fields that need to be filled in some way. Calculated fields need a form of processing behind them to work. The TFDMemTable, just like any other TDataSet descendant, has an event called OnCalcFields that allows the developer to do so. Create the OnCalcFields event handler on TFDMemTable and fill it with the following code:

    procedure TMainForm.FDMemTable1CalcFields(
    DataSet: TDataSet);
    var
      LPassedExams: Integer;
      LTotExams: Integer;
    begin
      LPassedExams := FDMemTable1.
        FieldByName('PassedExams').AsInteger;
      LTotExams := FDMemTable1.
        FieldByName('TotalExams').AsInteger;
      if LTotExams = 0 then
        FDMemTable1.FieldByName('PercPassedExams').AsFloat := 0
    else
         FDMemTable1.FieldByName('PercPassedExams').AsFloat := LPassedExams / LTotExams * 100;
    
    FDMemTable1.FieldByName('MoreThan50Percent').AsBoolean := FDMemTable1.FieldByName('PercPassedExams').AsFloat > 50;
           end;
         end;
  6. Run the application by hitting F9 (or by going to Run | Run) and you will get the following screenshot:

    Figure 3.1: A normal form with some data

  7. This is useful, but a bit boring. Let's start our customization. Close the application and return to the Delphi IDE.

  8. Go to the Properties of TDBGrid and set Default Drawing to False.

  9. Now, we've to organize the resources used to draw the grid cells. Calculated fields will be drawn directly using code, but the Rating field will be drawn using a 5-star rating image from 0 to 5. It starts with a 0.5 incremental step (0, 0.5, 1, 1.5, and so on). So, drop TImageList on the form, and set Height as 32 and Width as 160.

  10. Select the TImageList component and open the image list's editor by right-clicking and then selecting ImageList Editor. You can find the needed PNG images in the recipe project folder (ICONS\RATING_IMAGES). Load the images in the correct order as shown here:

    • Index 0 as image 0_0_rating.png

    • Index 1 as image 0_5_rating.png

    • Index 2 as image 1_0_rating.png

    • Index 3 as image 1_5_rating.png

    • Index 4 as image 2_0_rating.png

    Go to TDBGrid events and create the event handler for OnDrawColumnCell. All the customization code goes in this event.

    Include the Vcl.GraphUtil unit, and write the following code in the DBGrid1DrawColumnCell event:

    procedure TMainForm.DBGrid1DrawColumnCell(Sender: TObject;
     const Rect: TRect; DataCol: Integer;
      Column: TColumn; State: TGridDrawState);
    var
      LRect: TRect;
      LGrid: TDBGrid;
      LText: string;
      LPerc: Extended;
      LTextWidth: TSize;
      LSavedPenColor, LSavedBrushColor: Integer;
      LSavedPenStyle: TPenStyle;
      LSavedBrushStyle: TBrushStyle;
      LRating: Extended;
      LNeedOwnerDraw: Boolean;
    begin
      LGrid := TDBGrid(Sender);
      if [gdSelected, gdFocused] * State <> [] then
        LGrid.Canvas.Brush.Color := clHighlight;
    
      LNeedOwnerDraw := (Column.Field.FieldKind = fkCalculated) or Column.FieldName.Equals('Rating');
    
      if LNeedOwnerDraw then
      begin
        LRect := Rect;
        LSavedPenColor := LGrid.Canvas.Pen.Color;
        LSavedBrushColor := LGrid.Canvas.Brush.Color;
        LSavedPenStyle := LGrid.Canvas.Pen.Style;
        LSavedBrushStyle := LGrid.Canvas.Brush.Style;
    
        if Column.FieldName.Equals('PercPassedExams') then
        begin
          LText := FormatFloat('##0', Column.Field.AsFloat) + ' %';
          LGrid.Canvas.Brush.Style := bsSolid;
          LGrid.Canvas.FillRect(LRect);
          LPerc := Column.Field.AsFloat / 100 * LRect.Width;
          LGrid.Canvas.Font.Size := LGrid.Font.Size - 1;
          LGrid.Canvas.Font.Color := clWhite;
          LGrid.Canvas.Brush.Color := clYellow;
          LGrid.Canvas.RoundRect(LRect.Left, LRect.Top, Trunc(LRect.Left + LPerc), LRect.Bottom, 2, 2);
          LRect.Inflate(-1, -1);
          LGrid.Canvas.Pen.Style := psClear;
          LGrid.Canvas.Font.Color := clBlack;
          LGrid.Canvas.Brush.Style := bsClear;
    
          LTextWidth := LGrid.Canvas.TextExtent(LText);
          LGrid.Canvas.TextOut(LRect.Left + (
            (LRect.Width div 2) - (LTextWidth.cx div 2)), LRect.Top + ((LRect.Height div 2) - (LTextWidth.cy div 2)), LText);
        end
        else if Column.FieldName.
    Equals('MoreThan50Percent') then
        begin
          LGrid.Canvas.Brush.Style := bsSolid;
          LGrid.Canvas.Pen.Style := psClear;
          LGrid.Canvas.FillRect(LRect);
          if Column.Field.AsBoolean then
          begin
            LRect.Inflate(-4, -4);
            LGrid.Canvas.Pen.Color := clRed;
            LGrid.Canvas.Pen.Style := psSolid;
            DrawCheck(LGrid.Canvas, 
             TPoint.Create(LRect.Left, 
             LRect.Top + LRect.Height div 2),
             LRect.Height div 3);
          end;
        end
        else if Column.FieldName.Equals('Rating') then
        begin
          LRating := Column.Field.AsFloat;
          if LRating.Frac < 5 then
            LRating := Trunc(LRating);
          if LRating.Frac >= 5 then
            LRating := Trunc(LRating) + 0.5;
          LText := LRating.ToString;
          LGrid.Canvas.Brush.Color := clWhite;
          LGrid.Canvas.Brush.Style := bsSolid;
          LGrid.Canvas.Pen.Style := psClear;
          LGrid.Canvas.FillRect(LRect);
          Inc(LRect.Left);
          ImageList1.Draw(LGrid.Canvas, 
             LRect.CenterPoint.X - (ImageList1.Width div 2),
    		 LRect.CenterPoint.Y - (ImageList1.Height div 2),
    		 Trunc(LRating) * 2);
      end;
      end
      else
        LGrid.DefaultDrawColumnCell(Rect, DataCol, Column, State);
    
      if LNeedOwnerDraw then
      begin
        LGrid.Canvas.Pen.Color := LSavedPenColor;
        LGrid.Canvas.Brush.Color := LSavedBrushColor;
        LGrid.Canvas.Pen.Style := LSavedPenStyle;
        LGrid.Canvas.Brush.Style := LSavedBrushStyle;
      end;
    end;
  11. That's all folks! Hit F9 (or go to Run | Run), and we now have a nicer grid with more direct information about our data:

    Figure 3.2: The same grid with a bit of customization

How it works…

By setting the DBGrid property DefaultDrawing to False, we told the grid that we want to manually draw all the data into every cell. OnDrawColumnCell allows us to actually draw using standard Delphi code. For each cell we are about to draw, the event handler is called with a list of useful parameters to know which cell we're about to draw and what data we have to read considering the column we are currently drawing. In this case, we want to draw only the calculated columns and the Rating field in a custom way. This is not a rule, but this can be done to manipulate all cells. We can draw any cell in the way we like. For the cells where we don't want to do custom drawing, a simple call method, DefaultDrawColumnCell that passes the same parameters we got from the event and the VCL code will draw the current cell as usual.

Among the event parameters, there is a Rect object (of type TRect) that represents the specific area we're about to draw. There is a column object (of type TColumn) that is a reference to the current column of the grid and a State (of type TGridDrawState) that is a set of the grid cell states (for example, Selected, Focused, HotTrack, and many more). If our drawing code ignores the State parameter, all the cells will be drawn in the same way, and users cannot see which cell or row is selected.

The event handler uses a Pascal Sets Intersect to know whether the current cell should be drawn as a Selected or Focused cell. Refer the following code for better clarity:

  if [gdSelected, gdFocused] * State <> [] then
    Grid.Canvas.Brush.Color := clHighlight;

Tip

Remember that if your dataset has 100 records and 20 fields, OnDrawColumnCell will potentially be called 2000 times! So, the event code must be fast; otherwise, the application will become less responsive.

There's more…

Owner drawing is a really large topic and can be simple or tremendously complex, involving much Canvas-related code. However, often the kind of drawing you need will be relatively similar. So, if you need checks, arrows, color gradients, and so on, check the procedures into the Vcl.GraphUtil unit. Otherwise, if you need images, you could use TImageList to hold all the images needed by your grid, as we did in this recipe for the Rating field.

The good news is that the drawing code can be reused by different kinds of controls, so try to organize your code in a way that allows code reutilization by avoiding direct dependencies to the form where the control is.

The code in the drawing events should not contain business logic or presentation logic. If you need presentation logic, put it in a separate, testable function or class.

 

Using owner draw combos and listboxes


Many things are organized in a list. Lists are useful when you have to show items or when your user has to choose from a set of possible options. Usually, standard lists are flat, but sometimes, you need to transmit more information in addition to a list of items. Let's think about when you go to choose a font in an advanced text editor such as Microsoft Word or Apache OpenOffice. Having the name of the font drawn in the font style itself helps users make a faster and more reasoned choice. In this recipe, we'll see how to make listboxes more useful. The code is perfectly valid for TComboBox as well.

Getting ready

As we saw in the recipe, Customizing TDBGrid, many VCL controls are able to delegate their drawing, or part of it, to user code. It means that we can use simple event handlers to draw standard components in different ways. Let's say that we have a list of products in our store and we have to set discounts on these products. As there are many products, we want to make the processing in a way that our users can make a fast selection between the available discount percentages using a "color code."

How to do it…

  1. Create a brand new VCL application and drop a TListBox on the form. Set the following properties:

    Property

    Value

    Style

    lbOwnerDrawFixed

    Font.Size

    14

  2. In the listbox Items property, add seven levels of discount. For example, you can use no discount, 10 percent discount, 20 percent discount, 30 percent discount, 40 percent discount, 50 percent discount, 60 percent discount, and 70 percent discount.

  3. Then, drop a TImageList component on the form and set the following properties:

    Property

    Value

    ColorDepth

    cd32Bit

    DrawingStyle

    dsTransparent

    Width

    32

    Height

    32

  4. TImageList is our image repository and will be used to draw an image by index. Load 7 PNG images (size 32 x 32) into TImageList. You can find some nice PNG icons in the respective recipe project folder (ICONS\PNG\32).

  5. Create an OnDrawItem event handler for TListBox and write the following code:

    procedure TCustomListControlsForm.ListBox1DrawItem(
    Control: TWinControl; Index: Integer;
    Rect: TRect; State: TOwnerDrawState);
    var
      LBox: TListBox;
      R: TRect;
      S: string;
      TextTopPos, TextLeftPos, TextHeight: Integer;
    const
      IMAGE_TEXT_SPACE = 5;
    begin
      LBox := Control as TListBox;
      R := Rect;
      LBox.Canvas.FillRect(R);
      ImageList1.Draw(LBox.Canvas, R.Left, R.Top, Index);
      S := LBox.Items[Index];
      TextHeight := LBox.Canvas.TextHeight(S);
      TextLeftPos := R.Left + ImageList1.Width + IMAGE_TEXT_SPACE;
      TextTopPos := R.Top + R.Height div 2 - TextHeight div 2;
      LBox.Canvas.TextOut(TextLeftPos, TextTopPos, S);
    end;
  6. Run the application by hitting F9 (or by going to Run | Run) and you will see the following screenshot:

    Figure 4.1: Our listbox with some custom icons read from TImageList

How it works…

The TListBox.OnDrawItem event handler allows us to customize the drawing of the listbox. In this recipe, we've used TImageList as the image repository for the listbox. Using the Index parameter, we've read the correspondent image in TImageList and drawn on the listbox Canvas. After this, all the other code is related to the alignment of image and text inside the listbox row.

Remember that this event handler will be called for each item in the list, so the code must be fast and should not do too much slow Canvas writing. Otherwise, all your GUI will be unresponsive. If you want to create complex graphics "on the fly" in the event, I strongly suggest that you prepare your images the first time you draw the item and then put them in a sort of cache memory (TObjectList<TBitmap> is enough).

There's more…

While you are in OnDrawItem, you can do whatever you want with the TListBox Canvas. Moreover, the State parameter (of type TOwnerDrawState) tells you in which states the listbox item is (for example, Selected, Focused, HotTrack, and so on). So, you can use a different kind of drawing, depending on the item state. Check out the Customizing TDBGrid recipe to find out about the TDBGrid owner drawing for an example about the State parameter.

If you want to make your code aware of the selected VCL style, changing the color used according to it, you can use StyleServices.GetStyleColor(), StyleServices.GetStyleFontColor(), and StyleServices.GetSystemColor() in the Vcl.Themes unit.

The icons used in this recipe are from the Icojam website (http://www.icojam.com). The specific set used is available at http://www.icojam.com/blog/?p=259.

 

Making an owner draw control aware of the VCL styles


Owner draw controls are powerful. They allow you to completely tune your GUI for the needs of your users and potentially enable your application to display data in a more familiar way. In the end, owner draw controls improve the user experience with your application. However, owner draw controls do not always fit well with the VCL custom styles. Why? Because if you try to draw something by yourself, you could be tempted to use a "fixed" color, such clRed or clYellow, or you could be tempted to use the operating system color, such as clBtnFace or clWindow. Doing so, your owner draw controls will be not style aware and will be drawn in the same way regardless of the current VCL style. In this recipe, you'll learn how to make custom graphics remaining being in topic with the selected VCL style.

Getting ready

Let's say you are in charge of developing a controller panel for a hotel's light system. You have a list of lamps to power on, and you, using some hardware, have to power on some lamps by clicking on a button. Customers tell you that buttons should show some additional information about the lamp, for example:

  • Served zone (corridor, hall, room number, and so on)

  • State (on/off using some fancy graphics)

  • The time the lamp was powered on

  • The time when electrical problems have been detected, showing a red icon to indicate that the lamp is off even when current supplies the line, so the circuit is interrupted somewhere

  • Other custom information not currently known, such as small graphs showing lamp state history during the last 24 hours

The question is how to implement this kind of UI. One of the possible ways is to use TDrawGrid and draw all the needed details in each cell, using the cell also as a button. Using TDrawGrid, you have a grid of buttons for free. You have also the greatest flexibility about the information displayed because you are using the TCanvas method to custom draw each cell. This is quite a popular solution for this kind of non-standard UI. However, when you deploy this application, the customers ask about the possibility of changing the style of the application to fit the needs of the current user. So, you think about VCL styles, and you are right. However, the graphics drawn into the cells don't follow the currently selected VCL style, and your beautiful application becomes a bad mix of colors. In other words, when users change the selected VCL style, all the controls reflect the new style, but the owner drawn grid, which is unaware to the selected style, doesn't look as nice as the rest of the UI. How to solve this problem? How to draw custom graphics by adhering to the selected VCL style? In this recipe, you'll learn how to do it using the lamp control grid example.

How it works…

At design time, the form looks like the one shown in the following screenshot:

Figure 5.1 The form as it looks at design time

When the form is created, the list of available styles is loaded in the Radio group using code similar to the following one:

  RadioGroup1.Items.Clear;
  RadioGroup1.Columns := Length(TStyleManager.StyleNames);
  for LStyleName in TStyleManager.StyleNames do
    RadioGroup1.Items.Add(LStyleName);
  RadioGroup1.ItemIndex := 0;
  TStyleManager.SetStyle('Windows');

Then, a list of the TLampInfo object is created and initialized using the information contained in the Zones array. After that, the draw grid is initialized according to the LAMPS_FOR_EACH_ROW constant. Here's the relevant code:

  FLamps := TObjectList<TLampInfo>.Create(True);
  for I := 1 to LAMPS_FOR_EACH_ROW * 4 do
  begin
    FLamps.Add(TLampInfo.Create(Zones[I]));
  end;

  DrawGrid1.DefaultColWidth := 128;
  DrawGrid1.DefaultRowHeight := 64;
  DrawGrid1.ColCount := LAMPS_FOR_EACH_ROW;
  DrawGrid1.RowCount := FLamps.Count div LAMPS_FOR_EACH_ROW;

The FormCreate event handler initializes the styles list and the list of the lamps (the model) of the form. Now, we'll see how the other event handlers will use them.

The TDrawGrid OnSelectCell event, as the name suggests, is used to address the current "lamp" from the FLamps and to toggle its state. That's it. If the lamp is on, then the lamp will be powered down, else the lamp will be powered on. After that, the code forces the grid to redraw using the Invalidate method:

procedure TMainForm.DrawGrid1SelectCell(Sender: TObject; ACol, ARow: Integer; var CanSelect: Boolean);
begin
  FLamps[ACol + ARow * LAMPS_FOR_EACH_ROW].ToggleState;
  DrawGrid1.Invalidate;
end;

Now, really interesting things happened in the DrawThemed method called inside the TDrawGrid OnDrawCell event. This method receives information about the coordinates of the cell to draw, and then it draws a button on the canvas using the information contained in the correspond TLampInfo instance. The code is quite long, but an interesting concept is that no specific colors are used. When it is necessary to draw something, the code asks StyleService to get the correct color according to the current style. This approach is also used for font color and for system colors. Here's a handy table that summarizes these concepts:

Method name

Description

StyleServices.GetStyleColor(Color: TStyleColor)

Returns the color defined in the style for the element specified by Color

StyleServices.StyleFontColor(Font: TStyleFont)

Returns the font color for the element specified by Font

StyleServices.GetSystemColor(Color: TColor)

Returns the system color defined in the current style

So, when we have to highlight the (pseudo) button, if there are electrical problems on the power line, we will use the following code:

if LLamp.ThereAreElectricalProblems then
   LCanvas.Brush.Color := StyleServices.GetStyleColor(scButtonHot)
else
   LCanvas.Brush.Color := StyleServices.GetStyleColor(scWindow);
LCanvas.FillRect(LRect);

When we've got to draw normal text, we will use the following code:

LCanvas.Font.Color := StyleServices.GetStyleFontColor(sfButtonTextNormal);
LCanvas.TextRect(LRect, LValue, [TTextFormats.tfCenter, TTextFormats.tfVerticalCenter]);

It is clear that the paradigm is:

  • Get the current color for the selected element of the UI according to the style

  • Draw the graphics using that color

Clicking on the Simulate Problems button, it is possible to see how the graphics is drawn in the case of problems on the power line. The images are drawn directly from the image list using the following code:

procedure TMainForm.DrawImageOnCanvas(ACanvas: TCanvas; var ARect: TRect; ImageIndex: Integer);
begin
  ImageList1.Draw(ACanvas, ARect.Left + 4, ARect.Top + ((ARect.Bottom - ARect.Top) div 2) - 16, ImageIndex);
end;

Using this approach, the application created in this recipe, which has a lot of custom graphics, behaves very well even on VCL styles. Here are some screenshots:

Fig. 5.2 The application while it is using the Windows style

Fig. 5.3 The application while it is using the Luna style

Fig. 5.4 The application while it is using the Charcoal Dark Slate style

As you see, the application correctly draws the owner draw parts of the UI using the right colors from the selected style.

There's more…

The VCL style infrastructure is very powerful. In the case of TWinControl descendants, you can even define specific hooks for you components using TStyleHook. TStyleHook is a class that handles messages for controls acting as a wrapper for the hooked control. If you have a custom control that you want to be style enabled, inherit from TStyleHook and provide custom processing for that control. As examples, see TEditStyleHook and TComboBoxStyleHook. You need to register the style hook class with the style engine using the RegisterStyleHook method as shown in the following code:

TCustomStyleEngine.RegisterStyleHook(TCustomEdit, TEditStyleHook);

Moreover, the StyleServices function returns an instance of TCustomStyleServices, which provides a lot of customization methods related to the VCL styles. Check out the related documentation at http://docwiki.embarcadero.com/Libraries/en/Vcl.Themes.TCustomStyleServices_Methods to see all the possibilities

 

Creating a stack of embedded forms


Every modern browser has a tabbed interface. Also, many other kinds of "multiple views" software have this kind of interface. Why? Because it's very useful. While you are reading one page, you can rapidly check another page and still come back to the first one at the same point you left some seconds ago. You don't have to redo a search or use a lot of mouse clicks to just go back to that particular point. You simply have switched from one window to another window and back to the first. I have seen too many business applications that are composed of a bunch of dialog windows. Every form is called with the TForm.ShowModal method. So the user has to navigate into your application one form at time. This is simpler to handle for the programmer, but it's less user friendly for your customers. However, giving a "switchable" interface to your customer is not that difficult. In this recipe, we'll see a complete example of how to do it.

Getting ready

This recipe is a bit more complex than the previous recipes. So, I'll not explain all the code but only the fundamental parts. You can find the complete code in the book code repository (Chapter1\RECIPE06).

Let's say we want to create a tabbed interface for our software that is used to manage product orders, sales, and invoices. All the forms must be usable at the same time, without having to close the previous one. Before we begin, the following screenshot is what we want to create:

Figure 5.1: The main form containing seven embedded child forms

How it works…

The project is composed of a bunch of forms. The main form has TTabControl, which allows us to switch between the active forms. All embedded forms inherit from EmbeddableForm. The most important is the method Show shown here:

procedure TEmbeddableForm.Show(AParent: TPanel);
begin
  Parent := AParent;
  BorderStyle := bsNone;
  BorderIcons := [];
  Align := alClient;
  Show;
end;

Note

Note that all the forms apart from the main form, have been removed from the "Auto-Create Form" list (you can access the list by going to Project | Options | Forms).

All the other forms descend from EmbeddableForm and are added to TTabControl on the main form with a line of code similar to the following one:

procedure TMainForm.MenuOrdersClick(Sender: TObject);
begin
  AddForm(TForm1.Create(self));
end;

The AddForm method is in charge of adding an actual instance of a form into the tabs, keeping a reference to it. The following code shows how it is done:

//Add a form to the stack
procedure TMainForm.AddForm(
AEmbeddableForm: TEmbeddableForm);
begin
  AEmbeddableForm.Show(Panel1);
  //each tab show the caption of the containing form and   
  //hold the reference to it
  TabControl1.Tabs.AddObject(AEmbeddableForm.Caption, AEmbeddableForm);
  ResizeTabsWidth;
  ShowForm(AEmbeddableForm);
end;

Other methods are in charge of bringing an already created form to the front when a user clicks on the Related tab, and then to close a form when the Related tab is removed (check out the ShowForm and WMEmbeddedFormClose methods).

There is a bit of code, but the concepts are simple:

  • When we need to create a new form, add it in the TabControl1.Tabs property. The caption of the form is the caption of the tab, and the object is the form itself. This is what the AddForm method does with the following line:

    TabControl1.Tabs.AddObject(AEmbeddableForm.Caption, AEmbeddableForm);
  • When a user clicks on a tab, we have to find the associated form by cycling through the TabControl1.Tabs.Objects list and bringing it to the front.

  • When a form asks to be closed (sending a WM_EMBEDDED_CLOSE message), we have to set the ParentWantClose property and then call the Close method of the correspondent form.

  • If the user wants to close a form by closing the corresponding tab (in the recipe code, there is TPopMenu connected to TabControl, which is used to close a form with a right-click), we have to call the Close method on the corresponding form.

  • Every form frees itself in the OnClose event handler. This is done one time for all the forms in the TEmbeddableForm.CloseForm event handler, using the caFree action.

There's more…

Embedding a form into another TWinControl is not difficult and allows us to create flexible GUIs without using TPageControl and Frames. Probably, for the end user, this multi-tabbed GUI is probably more familiar because all the modern browsers use it, and probably, your user already knows how to use a browser with different pages or screens opened. From the developer's point of view, the multi-tabbed interface allows for much better programming patterns and practices. This technique can also be used for other scenarios where you have to embed one "screen" into another.

More flexible (and complex) solutions can be done involving the use of Observers, but in simple cases, this recipe's solution based on Windows Messaging is enough.

More information about the Observer design pattern can be found at http://sourcemaking.com/design_patterns/observer/delphi.

Another interesting solution (that doesn't rely on Windows Messaging and so is also cross-platform) may be based on the System.Messaging.TMessageManager class. More information about TMessageManager can be obtained at http://docwiki.embarcadero.com/Libraries/en/System.Messaging.TMessageManager.

Code in this recipe can be used with every component that uses TStringList to show items (TListBox, TComboBox, and so on) and can be adapted easily for other scenarios.

In the recipe code, you'll also find a nice way to show status messages generated by the embedded forms and a centralized way to show application hints in the status bar.

 

Manipulating JSON


JSON (JavaScript Object Notation) is a lightweight data-interchange format. As the reference site says, "It is easy for humans to read and write. It is easy for machines to parse and generate." It is based on a subset of the JavaScript programming language, but it is not limited to JavaScript in any way. Indeed, JSON is a text format that is completely language agnostic. These properties make JSON an ideal data-interchange language for many uses. In recent years, JSON has become on a par with XML in many applications, especially when the data size matters, because of its intrinsic conciseness and simplicity.

Getting ready

JSON provides the following five datatypes: String, Number, Object, Array, Boolean, and Null.

This simplicity is an advantage when you have to read a JSON string into some kind of language-specific structure, because every modern language supports the JSON datatypes as simple types or as HashMap (in the case of JSON objects) or List (in the case of JSON arrays). So, it makes sense that a data format that is interchangeable with programming languages is also based on these types and structures.

Since version 2009, Delphi provides built-in support for JSON. The System.JSON.pas unit contains all the JSON types with a nice object oriented interface. In this recipe, you'll see how to generate, modify, and parse a JSON string.

How to do it…

  1. Create a new VCL application and drop three TButton and a TMemo. Align all the buttons as a toolbar at the top of the form and the memo to all the remaining form client area.

  2. From left to right, name the buttons as btnGenerateJSON, btnModifyJSON, and btnParseJSON.

  3. We'll use static data as our data source. A simple matrix is enough for this recipe. Just after the start of the implementation section of the unit, write the following code:

    type
      TCarInfo = (
        Manufacturer = 1,
        Name = 2,
        Currency = 3,
        Price = 4);
    
    var
      Cars: array [1 .. 4] of array [Manufacturer .. Price] of string = (
        ('Ferrari','360 Modena','EUR', '250000'),
        ('Ford', 'Mustang', 'USD', '80000'),
        ('Lamborghini', 'Countach', 'EUR','300000'),
        ('Chevrolet', 'Corvette', 'USD', '100000')
      );
  4. TMemo is used to show our JSON files and our data. To keep things clear, create a public property called JSON on the form and map its setter and getter to the Memo1.Lines.Text property. Use the following code:

    //…other form methods declaration 
    private
      procedure SetJSON(const Value: String);
      function GetJSON: String;
    public
      property JSON: String read GetJSON write SetJSON;
    end;
    
    //…then in the implementation section
    function TMainForm.GetJSON: String;
    begin
      Result := Memo1.Lines.Text;
    end;
    
    procedure TMainForm.SetJSON(const Value: String);
    begin
      Memo1.Lines.Text := Value;
    end;
  5. Now, create event handlers for each button and write the code that follows. Pay attention to the event names:

    procedure TMainForm.btnGenerateJSONClick(Sender: TObject);
    var
      i: Integer;
      JSONCars: TJSONArray;
      Car, Price: TJSONObject;
    begin
      JSONCars := TJSONArray.Create;
      try
        for i := Low(Cars) to High(Cars) do
        begin
          Car := TJSONObject.Create;
          JSONCars.AddElement(Car);
          Car.AddPair('manufacturer', Cars[i][TCarInfo.Manufacturer]);
          Car.AddPair('name', Cars[i][TCarInfo.Name]);
          Price := TJSONObject.Create;
          Car.AddPair('price', Price);
          Price.AddPair('value', TJSONNumber.Create(Cars[i][TCarInfo.Price].ToInteger));
          Price.AddPair('currency',Cars[i][TCarInfo.Currency]);
        end;
        JSON := JSONCars.ToJSON;
      finally
        JSONCars.Free;
      end;
    end;
    
    procedure TMainForm.btnModifyJSONClick(Sender: TObject);
    var
      JSONCars: TJSONArray;
      Car, Price: TJSONObject;
    begin
      JSONCars := TJSONObject.ParseJSONValue(JSON) 
    as TJSONArray;
      try
        Car := TJSONObject.Create;
        JSONCars.AddElement(Car);
        Car.AddPair('manufacturer', 'Hennessey');
        Car.AddPair('name', 'Venom GT');
        Price := TJSONObject.Create;
        Car.AddPair('price', Price);
        Price.AddPair('value', TJSONNumber.Create(600000));
        Price.AddPair('currency', 'USD');
        JSON := JSONCars.ToJSON;
      finally
        JSONCars.Free;
      end;
    end;
    
    procedure TMainForm.btnParseJSONClick(Sender: TObject);
    var
      JSONCars: TJSONArray;
      i: Integer;
      Car, JSONPrice: TJSONObject;
      CarPrice: Double;
      s, CarName, CarManufacturer, CarCurrencyType: string;
    begin
      s := '';
      JSONCars := TJSONObject.ParseJSONValue(JSON) 
                                             as TJSONArray;
      if not Assigned(JSONCars) then
        raise Exception.Create('Not a valid JSON');
      try
        for i := 0 to JSONCars.Count - 1 do
        begin
          Car := JSONCars.Items[i] as TJSONObject;
          CarName := Car.GetValue('name').Value;
          CarManufacturer := Car.GetValue('manufacturer').Value;
          JSONPrice := Car.GetValue('price') as TJSONObject;
          CarPrice := (JSONPrice.GetValue('value') as TJSONNumber).AsDouble;
          CarCurrencyType := JSONPrice.GetValue('currency')
          .Value
          s := s + Format(
            'Name = %s' + sLineBreak +
            'Manufacturer = %s' + sLineBreak +
            'Price = %.0n%s' + sLineBreak +
            '-----' + sLineBreak,
            [CarName, CarManufacturer,
            CarPrice, CarCurrencyType]);
        end;
        JSON := s;
      finally
        JSONCars.Free;
      end;
    end;
  6. Run the application by hitting F9 (or by going to Run | Run).

  7. Click on the btnGenerateJSON button, and you should see a JSON array and some JSON objects in the memo.

  8. Click on the btnModifyJSON button, and you should see one more JSON object inside the outer JSON array in the memo.

  9. Click on the last button, and you should see the same data as before, but in a normal text representation.

  10. After the third click, you should see something similar to the following screenshot:

    Figure 6.1: Text representation of the JSON data generated and modified

There's more…

Although not the fastest or the most standard compliant on the market, JSON usability is important because other Delphi technologies, such as DataSnap, use it. Luckily, there are a lot of alternative JSON parsers for Delphi, if you find you have trouble with the standard one.

Other notable JSON parsers are:

If your main concern is speed, then check out these alternative JSON parsers.

There are also a lot of serialization libraries that use JSON as a serialization format. In general, every parser has its own way to serialize an object to JSON. Find your favorite. Just as an example, in Chapter 5, The Thousand Faces of Multithreading, in the Using tasks to make your customer happier recipe you will see an open source library containing a set of serialization helpers using the default Delphi JSON parser.

However, JSON is not the right tool for every interchange or data-representation job. XML has been creating other technologies that can help if you need to search, transform, and validate your data in a declarative way. In JSON land, there is no such level of standardization, apart from the format itself. However, over the years, there is an effort to include at least the XML Schema counterpart in JSON, and you can find more details at http://json-schema.org/.

 

Manipulating and transforming XML documents


XML stands for eXtensible Markup Language (http://en.wikipedia.org/wiki/XML) and is designed to represent, transport, and store hierarchical data in the trees of nodes. You can use XML to communicate with different systems, and store configuration files, complex entities, and so on. They all use a standard and powerful format. Delphi has had good support for XML for more than a decade now.

Getting ready

All the basic XML-related activities can be summarized with the following points:

  • Generate XML data

  • Parse XML data

  • Parse XML data and modify it

In this recipe, you will see how to carry out all these activities.

How to do it…

  1. Create a new VCL application and drop three TButton and a TMemo. Align all the buttons as a toolbar at the top of the form and the memo to the remaining form client area.

  2. From left to right, name the buttons btnGenerateXML, btnModifyXML, btnParseXML, and btnTransformXML.

  3. The real work on the XML will be done by the TXMLDocument component. So, drop one instance of the form and set its DOMVendor property to Omni XML.

  4. We will use static data as our data source. A simple matrix is enough for this recipe. Just after the implementation section of the unit, write the code that follows:

    type
      TCarInfo = (
      Manufacturer = 1,
      Name = 2,
      Currency = 3,
      Price = 4);
    
    var
      Cars: array [1 .. 4] of array [Manufacturer .. Price] of string = (
          (
            'Ferrari','360 Modena','EUR', '250,000'
          ),
          (
            'Ford', 'Mustang', 'USD', '80,000'
          ),
          (
            'Lamborghini', 'Countach', 'EUR','300,000'
          ),
          (
            'Chevrolet', 'Corvette', 'USD', '100,000'
          )
        );
  5. We will use a TMemo to display the XML and the data. To keep things clear, create a public property called Xml on the form and map its setter and getter to the Memo1.Lines.Text property. Use the following code:

    //…other form methods declaration 
    private
      procedure SetXML(const Value: String);
      function GetXML: String;
    public
      property Xml: String read GetXML write SetXML;
    end;
    
    //…then in the implementation section
    function TMainForm.GetXML: String;
    begin
      Result := Memo1.Lines.Text;
    end;
    
    procedure TMainForm.SetXML(const Value: String);
    begin
      Memo1.Lines.Text := Value;
    end;
  6. Now, create event handlers for each button. For btnGenerateXML, write the following code:

    procedure TMainForm.btnGenerateXMLClick(Sender: TObject);
    var
      RootNode, Car, CarPrice: IXMLNode;
      i: Integer;
      s: String;
    begin
      XMLDocument1.Active := True;
      try
        XMLDocument1.Version := '1.0';
        RootNode := XMLDocument1.AddChild('cars');
        for i := Low(Cars) to High(Cars) do
        begin
          Car := XMLDocument1.CreateNode('car');
          Car.AddChild('manufacturer').Text := Cars[i][TCarInfo.Manufacturer];
          Car.AddChild('name').Text := Cars[i][TCarInfo.Name];
          CarPrice := Car.AddChild('price');
          CarPrice.Attributes['currency'] := Cars[i][TCarInfo.Currency];
          CarPrice.Text := Cars[i][TCarInfo.Price];
          RootNode.ChildNodes.Add(Car);
        end;
        XMLDocument1.SaveToXML(s);
        Xml := s;
      finally
        XMLDocument1.Active := False;
      end;
    end;
  7. Now, we have to write the code to change the XML. In the btnModifyXML click event handler, write the following code:

    procedure TMainForm.btnModifyXMLClick(Sender: TObject);
    var
      Car, CarPrice: IXMLNode;
      s: string;
    begin
      XMLDocument1.LoadFromXML(Xml);
      try
        Xml := '';
        Car := XMLDocument1.CreateNode('car');
        Car.AddChild('manufacturer').Text := 'Hennessey';
        Car.AddChild('name').Text := 'Venom GT';
        CarPrice := Car.AddChild('price');
        CarPrice.Attributes['currency'] := 'USD';
        CarPrice.Text := '600,000';
        XMLDocument1.DocumentElement.ChildNodes.Add(Car);
        XMLDocument1.SaveToXML(s);
        Xml := s;
      finally
        XMLDocument1.Active := False;
      end;
    end;
  8. Write the following code under the btnParseXML click event handler:

    procedure TMainForm.btnParseXMLClick(Sender: TObject);
    var
      CarsList: IDOMNodeList;
      CurrNode: IDOMNode;
      childidx, i: Integer;
      CarName, CarManufacturer, CarPrice, CarCurrencyType: string;
    begin
      XMLDocument1.LoadFromXML(Xml);
      try
        Xml := '';
        CarsList := XMLDocument1.DOMDocument.getElementsByTagName('car');
        for i := 0 to CarsList.length - 1 do
        begin
          CarName := '';  CarManufacturer := '';
          CarPrice := '';  CarCurrencyType := '';
          for childidx := 0 to CarsList[i].ChildNodes.length - 1 do
          begin
            CurrNode := CarsList[i].ChildNodes[childidx];
            if CurrNode.nodeName.Equals('name') then
              CarName := CurrNode.firstChild.nodeValue;
            if CurrNode.nodeName.Equals('manufacturer') then
              CarManufacturer := CurrNode.firstChild.nodeValue;
            if CurrNode.nodeName.Equals('price') then
            begin
              CarPrice := CurrNode.firstChild.nodeValue;
              CarCurrencyType := CurrNode.Attributes.getNamedItem('currency').nodeValue;
            end;
          end;
          Xml := Xml + 'Name = ' + CarName + sLineBreak + 'Manufacturer = ' + CarManufacturer + sLineBreak + 'Price = ' + CarPrice + CarCurrencyType + sLineBreak + '-----' + sLineBreak;
        end;
      finally
        XMLDocument1.Active := False;
      end;
    end;
  9. Finally, write the following code under the btnTransformXML click event handler:

    procedure TMainForm.btnTransformClick(Sender: TObject);
    var
      LXML, LXSL: string;
      LOutput: string;
    begin
      LXML := TFile.ReadAllText('..\..\..\cars.xml');
      LXSL := TFile.ReadAllText('..\..\..\cars.xslt');
      LOutput := Transform(LXML, LXSL);
      TFile.WriteAllText('..\..\..\cars.html', LOutput);
      ShellExecute(0, PChar('open'), PChar('file:///' + TPath.GetFullPath('..\..\..\cars.html')), nil, nil, SW_SHOW);
    end;
  10. Now, add the following function in your form implementation section:

    function Transform(XMLData: string; XSLT: string): String;
    var
      LXML, LXSL: IXMLDocument;
      LOutput: WideString;
    begin
      LXML := LoadXMLData(XMLData);
      LXSL := LoadXMLData(XSLT);
      LXML.DocumentElement.TransformNode(LXSL.DocumentElement, LOutput);
      Result := String(LOutput);
    end;
  11. Run the application by hitting F9 (or by going to Run | Run).

  12. Click on the btnGenerateXML button, and you should see some XML data in the memo.

  13. Click on the btnModifyXML button, and you should see some more XML in the memo.

  14. Click on btnParseXML, and you should see the same data as before, but in normal text representation.

  15. After the third click, you should see something similar to the following screenshot:

    Figure 7.1: Text representation of the XML data generated and modified

  16. Now, copy the cars.xml and cars.xslt files from the respective recipe folder to the parent folder of your project folder and click on the btnTransformXML button.

  17. The system default browser should appear showing, something like the following screenshot:

    Fig. 7.2 XML data transformed to HTML using a XSLT transformation

How it works…

  1. The first button generates the XML representation of the data in our matrix. We've used some car information as sample data.

    Note

    Note that the prices of the cars are not real!!

  2. To create an XML attribute, there are three fundamental TXMLDocument methods:

    • XMLNode := XMLDocument1.CreateNode('node');

    • XMLNode.AddChild('childnode');

    • XMLNode.Attributes['attrname'] := 'attrvalue';

    There are other very useful methods, but these are the basics of XML generation.

  3. The btnModifyXML button loads the XML into the memo and appends some other data (another car) to the list. Then, it updates the memo with the new updated XML. These are the most important lines to note:

    //Create a node without adding it to the DOM
    Car := XMLDocument1.CreateNode('car');
    
    //fill Car XMLNode… and finally add it to the DOM
    //as child of the root node
    XMLDocument1.DocumentElement.ChildNodes.Add(Car);
  4. The code under the btnParseXMLClick event handler allows us to read the display as normal text the XML data navigating through XML tree.

  5. The code under the btnTransformXMLClick event handler uses the XSLT transformation in cars.xslt and the data in cars.xml to generate a brand new HTML page. The XSLT code is as follows:

    <?xml version="1.0" encoding="UTF-8"?>
    <xsl:stylesheet version="1.0"  
      xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
        <xsl:output method="html" version="5.0" encoding="UTF-8" indent="yes"/>
      <xsl:template match="cars">
        <html>
          <head>
            <link href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.4/css/bootstrap.min.css" rel="stylesheet"/>
            <title>
              Sport Cars
            </title>
          </head>
          <body>
            <div class="container">
            <div class="row">
            <h1>Sport Cars</h1>
              <table class="table table-striped table-hover">
                <thead>
                  <tr>
                    <th>Model</th>
                    <th>Manufacturer</th>
                    <th class="text-right">Price</th>
                  </tr>
                </thead>
                <tbody>
                  <xsl:for-each select="car">
                  <tr>
                    <td>
                      <xsl:value-of select="name"/>
                    </td>
                    <td>
                      <xsl:value-of select="manufacturer"/>
                    </td>
                    <td class="text-right">
                      <span class="glyphicon glyphicon-euro">
                      </span>
                      <xsl:value-of select="price"/>
                    </td>
                  </tr>
                </xsl:for-each>
              </tbody>
            </table>
          </div>
          </div>
        </body>
      </html>
    </xsl:template>
    </xsl:stylesheet>

There's more…

There are many things to say about XML ecospace. There are XML engines that provide facilities to search data in an XML tree (XPath), to validate an XML using another XML (XML Schema or DTD), to transform an XML into another kind of format using another XML (XSLT), and many others use http://en.wikipedia.org/wiki/List_of_XML_markup_languages. The good thing is that just like XML, the DOM object is also standardized. So, every library that is compliant to the standard has the same methods, from Delphi to JavaScript and from Python to C#.

TXMLDocument allows you to select the DOMVendor implementation. By default, there are three implementations available:

  • MSXML:

    • Is from Microsoft, implemented as COM objects

    • Supports XML transformations

    • Is available only on Windows (so no Android, iOS, or MacOSX)

  • Omni XML:

    • Much faster than ADOM and based on the Open Source project.

    • It is cross-platform, so is available on all the supported Delphi platforms. If you plan to write XML handling code on mobile or Mac, this is the way to go.

  • ADOM XML:

    • Is a (quite old) open source Delphi implementation

    • Does not support transformations

    • Is available on all the supported Delphi platforms

    • Is still in Delphi for backward compatibility, consider the Omni XML instead

TXMLDocument uses a Windows-only vendor by default. If you are designing a FireMonkey application that is intended to run on other platforms than Windows, select a cross-platform DOM vendor.

XSLT allows you to transform an XML to something else, using another XML as a "stylesheet." As we saw in this recipe, you can use an XML file and an XSLT file to generate an HTML page that shows the data contained in the XML using XSLT to format the data.

The following function loads the XML and an XSLT documents from two string variables. Then, we use the XSLT document to transform the XML document. The code that follows shows this in detail:

function Transform(XMLData: string; XSLT: string): String;
var
  LXML, LXSL: IXMLDocument;
  LOutput: WideString;
begin
  LXML := LoadXMLData(XMLData);
  LXSL := LoadXMLData(XSLT);
  LXML.DocumentElement.TransformNode(
    LXSL.DocumentElement, LOutput);
  Result := String(LOutput);
end;

This function doesn't know about the output format because it is defined by the XSLT document. The result could be an XML, an HTML, a CSV, or a plain text, or whatever the XSLT defines, but the code does not change.

XSLT can be really useful. I recommend that you go and visit http://www.w3schools.com/xsl/xsl_languages.asp for further details on the language.

 

I/O in the 21st century – knowing the streams


Many I/O-related activities handle "streams" of data. A stream is a sequence of data elements made available over time. As Wikipedia says, "A stream can be thought of as a conveyor belt that allows items to be processed one at a time rather than in large batches."

At the lowest level, all streams are bytes, but using a high-level interface could obviously help the programmer handle their data. This is the reason why a stream object usually had methods such as read, seek, write, and so on, just to make handling a byte stream a bit simpler.

In this recipe, you'll see some stream utilization examples.

Getting ready

In the good old Pascal days, there were a set of functions to handle the I/O (AssignFile, Reset, Rewrite, CloseFile, and many more). Now, we've a bunch of classes. All Delphi streams inherit from TStream and can be used as the internal stream of one of the adapter classes (by adapter, I mean an implementation of the Adapter, or Wrapper, design pattern from the Gang of Four famous book about design patterns).

There are 10 fundamental types of streams.

Class

Use

System.Classes.TBinaryWriter

Writer for binary data

System.Classes.TStreamWriter

Writer for characters to stream

System.Classes.TStringWriter

Writer for a string

System.Classes.TTextWriter

Writer of sequence of characters; it is an abstract class

System.Classes.TWriter

Writes component data to an associated stream

System.Classes.TReader

Reads component data from an associated stream

System.Classes.TStreamReader

Reader for stream of characters

System.Classes.TStringReader

Reader for strings

System.Classes.TTextReader

Reader for sequence of characters; it is an abstract class

System.Classes.TBinaryReader

Reader for binary data

You can check out the complete list and their intended use directly on the Embarcadero website at http://docwiki.embarcadero.com/RADStudio/en/Streams,_Reader_and_Writers.

As Joel Spolsky says, "You can no longer pretend that "plain" text is ASCII." So, while we write streams, we've to pay attention to which encoding our text has and which encoding our counterpart is waiting for.

One of the most frequent necessities is to efficiently read and write a text file using the correct encoding.

 

"The Single Most Important Fact About Encodings… It does not make sense to have a string without knowing what encoding it uses. You can no longer stick your head in the sand and pretend that "plain" text is ASCII."

 
 --Joel Spolsky (http://www.joelonsoftware.com/articles/Unicode.html)

The point Joel is making is that the content of a string doesn't know about the type of character encoding it uses.

When you think about file handling, ask yourself, "Could this file become 10 MB? And 100 MB? And 1 GB? How will my program behave in that case?" Handling a file one line at time and not loading all the file contents in memory is usually a good insurance for these cases. A stream of data is a good way to do this. In this recipe, you'll see the practical utilization of streams, stream writers, and stream readers.

How it works…

The project is not complex. All the interesting stuff happens in btnWriteFile and btnReadFile.

To write the file, TStreamWriter is used. TStreamWriter (as its counterpart TStreamReader) is a wrapper for a TStream descendant and adds some useful high-level methods to write to the stream. There are a lot of overloaded methods (Write/WriteLine) to allow an easy writing to the underlying stream. However, you can access the underling stream using the BaseStream property of the wrapper. Just after having written the file, the memo reloads the file using the same encoding used to write it, and shows it. This is only a fast check for this recipe, you don't need TMemo at all in your real project. The btnReadFile simply opens the file using a stream and passes the stream to TStreamReader that, using the right encoding, will read the file one line at a time.

Now, let's run some checks. Run the program and with the encoding set to ASCII, click on btnWriteFile. The memo will show garbage text, as shown in the following screenshot. This is because we are using the wrong encoding for the data we are writing in the file:

Figure 8.1: Garbage text written to the file using the wrong encoding. No one line text is equal to the original one. It is necessary to know the encoding for the text before writing and reading it

Now, select UTF8 from the RadioGroup and retry. By clicking on btnWriteFile, you will see the correct text in the memo. Try to change the current encoding using ASCII and click on btnReadFile. You will still get garbage text. Why? Because the file has been read with the wrong encoding. You have to know the encoding before to safely read file's contents. To read the text that we wrote, we have to use the same encoding. Play with other encodings to see the different behavior.

There's more…

Streams are very powerful and their uniform interface helps us write portable and generic code. With the help of streams and polymorphism, we can write code that uses TStream to do some work without knowing which kind of stream it is!

Also, a less known possibility, if you ever will write a program that needs to access the good old STD_INPUT, STD_OUTPUT, or STD_ERROR, is that you can use THandleStream to wrap these system handles to a nice TStream interface with the following code:

program StdInputOutputError;
//the following directive instructs the compiler to create a 
//console application and not a GUI one, which is the default.
{$APPTYPE CONSOLE} 
uses
  System.Classes, // required for Stream classes
  Winapi.Windows; // required to have access to the STD_* handles
var
  StdInput: TStreamReader;
  StdOutput, StrError: TStreamWriter;
begin
  StdInput := TStreamReader.Create(
THandleStream.Create(STD_INPUT_HANDLE));
  StdInput.OwnStream;
  StdOutput := TStreamWriter.Create(
THandleStream.Create(STD_OUTPUT_HANDLE));
  StdOutput.OwnStream;
  StdError := TStreamWriter.Create(
THandleStream.Create(STD_ERROR_HANDLE));
  StdError.OwnStream;
  { HERE WE CAN USE OURS STREAMS }
  // Let's copy a line of text from STD_IN to STD_OUT
  StdOutput.writeln(StdInput.ReadLine);
  { END - HERE WE CAN USE OURS STREAMS }
  StdError.Free;
  StdOutput.Free;
  StdInput.Free;
end;

Moreover, when you work with file-related streams, the TFile class (contained in System.IOUtils.pas) is very useful, and has some helper methods to write shorter and more readable code.

 

Creating a Windows service


Some kinds of application needs to be running 24/7. Usually, they are network servers or data transfer/monitoring applications. In these cases, you probably start with a normal GUI or console application. However, when the systems start to be used in production, you are faced with a lot of problems related to Windows session termination, reboots, user rights, and other issues related to the server environment.

Getting ready

The way to go, in the previous scenario, is to develop a Windows service. In this recipe, we'll see how to write a good Windows service scaffold, and this can be the skeleton for many other services. So, feel free to use this code as a "template" to create all the services that you will need.

How it works…

The project has been created starting from the default project template accessible by going to File | New | Other | Delphi Projects | Service Application and then has been integrated with a set of functionalities to make it "real."

All the low-level interfacing with the Windows Service Manager is done by the TService class. In ServiceU.pas, there is the actual descendant of TService that represents the Windows service we are implementing. Its event handlers are used to communicate with the operating system.

Usually, a service needs to respond to the Windows Service Controller commands independently of what it is doing. So, we need a background thread to do the actual work, while the TService.OnExecute event should not do any real work (this is not a must, but usually is the way to go). The unit named WorkerThreadU.pas contains the thread and the main service needed to hold a reference to the instance of this thread.

The background thread starts when the service is started (the OnStart event) and stops when the service is stopped (the OnStop event). The OnExecute event waits and handles ServiceController commands but doesn't do any actual functional work. This is done using ServiceThread.ProcessRequests(false); in a while loop.

Usually the OnExecute event handler is like this:

procedure TSampleService.ServiceExecute(Sender: TService);
begin
  while not Terminated do
  begin
    ServiceThread.ProcessRequests(false);
    TThread.Sleep(1000); 
  end;
end;

The wait of 1000 milliseconds is not a must, but consider that the wait time should be not too high because the service needs to be responsive to the Windows Service Controller messages. It should not be too low because otherwise the thread context switch may waste resources.

The background thread writes a line in a logfile once a second. While it is in a Paused state, the service stops writing. When the service continues, the thread will restart writing the log line. In the service event handlers, there is the logic to implement this change of state:

procedure TSampleService.ServiceContinue(Sender: TService; var Continued: Boolean);
begin
FWorkerThread.Continue;
Continued := True;
end;

procedure TSampleService.ServicePause(Sender: TService; var Paused: Boolean);
begin
FWorkerThread.Pause;
Paused := True;
end;

In the thread, there is the actual logic to implement the Paused state, and in this case, it is fairly simple: we've to pause the writing of the logfile:

Here's an extract:

    Log := TStreamWriter.Create(
      TFileStream.Create(LogFileName, fmCreate or fmShareDenyWrite));
    try
      while not Terminated do
      begin
        if not FPaused then
        begin
          Log.WriteLine('Message from thread: ' + TimeToStr(now));
        end;
        TThread.Sleep(1000);
      end;
    finally
      Log.Free;
    end;

The Boolean instance variable FPaused can be considered thread safe for this use.

Delphi services don't have a default description under the Windows Service Manager. If we want to give a description, we have to write a specific key in the Windows registry. Usually, this is done in the AfterInstall event. In our service, this is the code to write in the AfterInstall event handler:

procedure TSampleService.ServiceAfterInstall(
Sender: TService);
var
Reg: TRegistry; //declared in System.Win.Registry;
begin
Reg := TRegistry.Create(KEY_READ or KEY_WRITE);
try
  Reg.RootKey := HKEY_LOCAL_MACHINE;
  if Reg.OpenKey(
    '\SYSTEM\CurrentControlSet\Services\' + name, 
    False {do not create if not exists}) then
  begin
    Reg.WriteString('Description', 
      'My Fantastic Windows Service');
    Reg.CloseKey;
  end;
finally
  Reg.Free;
end;
end;

It is not necessary to delete this key in the AfterUnInstall event because Windows deletes all the keys related to the service (under HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\<MyServiceName>) when it is actually uninstalled.

Let's try an installation. Build the project, open the Windows command prompt, and go to the folder where the project has been built. Then, run this command:

C:\<ExeProjectPath>\WindowsService.exe /install

If everything is ok, you should see this message:

Figure 11.1: The service installation is ok

Now, you can check under the Windows Services Console and you should find the service installed. Click on Start, wait for the confirmation, and the service should start to write its logfile.

Play with Pause and Continue and check the file activity.

Note

Some text editors could have a problem with opening the logfile while the service is writing. I suggest that you use a Unix tail clone for Windows.

There are many free choices. Here are some links:

http://sourceforge.net/projects/tailforwin32/

http://ophilipp.free.fr/op_tail.htm

http://www.baremetalsoft.com/baretail/

There's more…

Windows Services are very powerful. Using the abstractions that Delphi provides, you can also create an application that, reading a parameter on the command line, can act as a normal GUI application or as a Windows Service.

In the respective recipe folder, there is another recipe called 20_WindowsServiceOrGUI.

This application can be used as a normal Windows Service using the normal command line switches used so far, but if launched with /GUI, it acts as a GUI application and can use the same application code (not TService). In our example, the GUI version uses the same worker thread as the service version. This can be very useful also for debugging purposes.

Run the application with the following command:

C:\<ExeProjectPath>\WindowsServiceOrGUI.exe /GUI

You will get a GUI version of the service, as shown here:

Figure 11.2: The GUI version of the Windows Service

Using the TService.LogMessage method

If something happens during the execution of the service that you want to log and you want to log in to the system logger, you can use the LogMessage method to save a message. The message can be viewed later using the Windows built-in event viewer.

You can call the LogMessage method using an appropriate logging type like this:

LogMessage('Your message goes here for SUCCESS', EVENTLOG_SUCCESS, 0, 1);

If you check the event in the Event Viewer, you will find a lot of garbage text that complains about the lack of "description for the event."

If you really want to use the Event Viewer to view your log message (when I can, I use a logfile and don't care about the Event Viewer, but there are scenarios where the Event Viewer log is needed), you have to use the Microsoft © Message Compiler.

The Microsoft © Message Compiler is a tool able to compile a file of messages into a set of RC files. Then, these files must be compiled by a resource compiler and linked into your executable.

More information on Microsoft © Message Compiler and the steps needed to provide the description for the log event can be found at http://www.codeproject.com/Articles/4166/Using-MC-exe-message-resources-and-the-NT-event-lo.

 

Associating a file extension with your application on Windows


In some cases, your fantastic application needs to be opened with just a double-click on a file with an extension associated with it. This is the case with Microsoft Word, Microsoft Excel, and many other well-known pieces of software. If you have a file generated with a program, double-click on the file and the program that generated the file will bring up pointing to that file. So, if you click on mywordfile.docx, Microsoft Word will be opened and mywordfile.docx will be shown. This is what we'd like to do in this recipe. The association can be useful also when you have multiple configurations for a program. Double-click on the ConfigurationXYZ.myext file, and the program will start using that configuration.

Getting ready

The hard work is done by the operating system itself. We have to instruct Windows to provide the following information:

  • The file extension to associate

  • The description of file type (it will be shown by Windows Explorer describing the file type)

  • The default icon for the file type (in this recipe, we'll use the application icon itself, but it is not mandatory)

  • The application that we want to associate

  • Let's start!

How to do it…

  1. Create a new VCL application and drop two TButton components and a TMemo component. Align all the buttons as a toolbar at the top of the form and the memo to all the remaining form client area.

  2. The button on the left-hand side will be used to register a file type, while the button on the right-hand side will be used to unregister the association (cleaning the registry).

  3. We have to handle some features specific to Microsoft Windows, so we need some Windows-related units. Under the implementation section of the unit, write this use clause:

    uses System.Win.registry, Winapi.shlobj, System.IOUtils;
  4. In the implementation section, we need two procedures to do the real work; so just after the uses clause, add this code:

    procedure UnregisterFileType(
      FileExt: String; 
      OnlyForCurrentUser: boolean = true);
    var
      R: TRegistry;
    begin
      R := TRegistry.Create;
      try
        if OnlyForCurrentUser then
          R.RootKey := HKEY_CURRENT_USER
        else
          R.RootKey := HKEY_LOCAL_MACHINE;
    
        R.DeleteKey('\Software\Classes\.' + FileExt);
        R.DeleteKey('\Software\Classes\' + FileExt + 'File');
      finally
        R.Free;
      end;
      SHChangeNotify(SHCNE_ASSOCCHANGED, SHCNF_IDLIST, 0, 0);
    end;
    
    procedure RegisterFileType(
      FileExt: String;
      FileTypeDescription: String;
      ICONResourceFileFullPath: String;
      ApplicationFullPath: String;
      OnlyForCurrentUser: boolean = true);
    var
      R: TRegistry;
    begin
      R := TRegistry.Create;
      try
        if OnlyForCurrentUser then
          R.RootKey := HKEY_CURRENT_USER
        else
          R.RootKey := HKEY_LOCAL_MACHINE;
    
        if R.OpenKey('\Software\Classes\.' + FileExt, true) then begin
          R.WriteString('', FileExt + 'File');
          if R.OpenKey('\Software\Classes\' + FileExt + 'File', true) then begin
            R.WriteString('', FileTypeDescription);
            if R.OpenKey('\Software\Classes\' + FileExt + 'File\DefaultIcon', true) then
            begin
              R.WriteString('', ICONResourceFileFullPath);
              if R.OpenKey('\Software\Classes\' + FileExt + 'File\shell\open\command', true) then
              R.WriteString('', ApplicationFullPath + ' "%1"');
              end;
            end;
          end;
        finally
        R.Free;
      end;
      SHChangeNotify(SHCNE_ASSOCCHANGED, SHCNF_IDLIST, 0, 0);
    end;
  5. These two procedures allow us to register (and unregister) a file type considering only the current user or all the machine users. Pay attention; if you want to register the association for every user, write your data to:

    HKEY_LOCAL_MACHINE\Software\Classes
  6. If you want to register the association for the current user only, write your data to:

    HKEY_CURRENT_USER\Software\Classes
  7. On the newest Windows versions, you need administrator rights to register a file type for all the machine users. The last line of the procedures tells Explorer (the Microsoft Windows graphic interface) to refresh its settings to reflect the changes made to the file associations. As a result, for instance, the Explorer file list views will update.

  8. We've almost finished. Change the left button name to btnRegister, the right button name to btnUnRegister, and put the following code on their onclick event handlers:

    procedure TMainForm.btnRegisterClick(Sender: TObject);
    begin
      RegisterFileType(
        'secret',
        'This file is a secret',
        Application.ExeName,
        Application.ExeName,
        true);
      ShowMessage('File type registred');
    end;
    
    procedure TMainForm.btnUnRegisterClick(Sender: TObject);
    begin
      UnregisterFileType('secret', true);
      ShowMessage('File type unregistered');
    end;
  9. Now, when our application is invoked with a double-click, we'll get the file name as a parameter. It is possible to read a parameter passed by Windows Explorer (or the command line) using the ParamStr(1) function. Create a FormCreate event handler using the following code:

    procedure TMainForm.FormCreate(Sender: TObject);
    begin
      if TFile.Exists(ParamStr(1)) then
        Memo1.Lines.LoadFromFile(ParamStr(1))
      else
      begin
        Memo1.Lines.Text := 'No valid secret file type';
      end;
    end;
  10. Now, the application should be complete. However, a nice integration with the operating system requires a nice icon. In the code, the associated file will get the same icon as the main program, so let's change our default icon by going to Project | Options | Application dialog, and choose a nice icon. Click on the Load Icon button, choose an ICO file, and then select the third item from the resultant dialog:

    Figure 12.1: Changing the default application icon for our application

  11. Now, create some text files with our registered extension .secret.

  12. These files will appear with the default Windows icons, but in some seconds, they will have a brand new icon.

  13. Run the application by hitting F9 (or by going to Run | Run).

  14. Click on the btnRegister button and close the application. Now, the files get new icons, as shown here:

    Figure 12.2: The files in Windows Explorer before and after having registered the .secret extension

  15. Now, with the application not running, double-click on a .secret file. Our program will be started by Windows itself, using the information stored in the registry about the .secret file, and we'll get this form (the text shown in the memo is the text contained in the file):

    Figure 12.3: Our application, launched by the operating system, while it is showing the contents of the file

There's more…

One application can register many file types. In some cases, I've used this technique to register some specific desktop database files to my application (Firebird SQL Embedded database files or SQLite database files). So, a double-click actually was a connection to that database.

 

Be coherent with the Windows look and feel using TTaskDialog


Version after version, the Windows OS changed its look and feel a lot from the mid 2009 when the first Windows 95 came out. Also the UX guidelines from Microsoft changed a lot. Do you remember the Multiple Document Interface (MDI) paradigm? It was very popular in the 90s, but now is deprecated and an application seems old also if it has been just released. Indeed, many Windows applications seem stuck in the past in terms of UI and UX. What about dialogs? Our beloved ShowMessage and MessageDlg are there since Delphi 1, but now, the modern Windows versions use different dialogs to communicate to the users. Many of these standard dialogs contain more than a question and a simple Yes and No. Some dialogs ask something and provide a list of choices using radio buttons; some others have a nice progress bar inside; others have a nice button with an extended explanation of each choice just inside the button. How can our Delphi application can benefit from these new dialogs offered by the OS? In other words, how we can give a coherent look and feel to our dialog windows so that our application does not look old? This recipe shows how to use the TTaskDialog component.

Getting started

TTaskdialog is a dialog box somewhat like the standard call to Application.MessageBox in the VCL but much more powerful. Task Dialog API has been available since Windows Vista and Windows Server 2008, and your application must be theme enabled to use it (go to Project | Options | Application | Runtime Themes | Enable Runtime Themes).

Besides the usual default set of buttons (OK, Cancel, Yes, No, Retry, and Close), you can define extra buttons and many other customizations. The following Windows API provides task dialogs:

API Name

Description

TaskDialog

This creates, displays, and operates a task dialog. The task dialog contains application-defined message text and title, icons, and any combination of predefined push buttons. This function does not support the registration of a callback function to receive notifications.

TaskDialogCallbackProc

This is an application-defined function used with the TaskDialogIndirect function. It receives messages from the task dialog when various events occur. TaskDialogCallbackProc is a placeholder for the application-defined function name.

TaskDialogIndirect

This creates, displays, and operates a task dialog. The task dialog contains application-defined icons, messages, title, verification checkbox, command links, push buttons, and radio buttons. This function can register a callback function to receive notification messages.

More information about API utilization can be obtained from https://msdn.microsoft.com/en-us/library/windows/desktop/bb787471(v=vs.85).aspx.

While the API can be useful in some border cases, the VCL comes with a very nice component that does all the low-level stuff for us. Let's see the sample program that shows how it is simple to create a modern look and feel application.

How it works…

Open the TaskDialogs.dproj project and understand how it works.

There are six buttons on the form. The first one shows a simple utilization of the Task Dialog API, while the other five show a different utilization of the TTaskDialog component, which wraps that API.

The first button uses the Windows API directly with the following code:

procedure TMainForm.btnAPIClick(Sender: TObject);
var
  LTDResult: Integer;
begin
  TaskDialog(0, HInstance,
    PChar('The Title'),
    PChar('These are the main instructions'),
    PChar('This is another content'),
    TDCBF_OK_BUTTON or TDCBF_CANCEL_BUTTON,
    TD_INFORMATION_ICON, @LTDResult);
  case LTDResult of
    IDOK:
      begin
        ShowMessage('Clicked OK');
      end;
    IDCANCEL:
      begin
        ShowMessage('Clicked Cancel');
      end;
  end;
end;

The TaskDialog function is declared inside the Winapi.CommCtrl.pas unit. So far, you could ask, "Why should I use a component for TaskDialogs? Seems quite simple." Yes, it is, if you only want to mimic MessageDlg, but things get complicated very fast if you want to use all the features of the Task Dialog API. So, the second button uses the TTaskDialog component. Let's see the relevant properties configured at design time for the tdSimple component:

object tdSimple: TTaskDialog
  Caption = 'The question'
  CommonButtons = [tcbYes, tcbNo]
  DefaultButton = tcbYes
  ExpandButtonCaption = 'More information'
  ExpandedText = 'Yes, you have to decide something about this question...' + ' but I cannot help you a lot'
  Flags = [tfUseHiconMain, tfUseHiconFooter, tfVerificationFlagChecked]
  FooterIcon = 4
  FooterText = 'This is an important question...'
  Text = 'To be or not to be, this is the question. To be?'
  Title = 'William ask:'
end

Note

You can check the runtime appearance also at design time by double-clicking on the component over your form, or by selecting Test Dialog from the menu over the component. You can access the menu by right-clicking on the component.

As you can see, only the minimum properties have been set, just to show the power of the component. This configuration shows up a dialog with two buttons labelled Yes and No. The TTaskDialog component can be configured at design time using the Object Inspector, or can be configured at runtime by code. In this first example, the configuration is defined at design time so that at runtime we only have to call the Execute method and read the user response. Here's the code that actually uses the tdSimple instance:

procedure TMainForm.btnSimpleClick(Sender: TObject);
begin
  tdSimple.Execute; //show the taskdialog
  if tdSimple.ModalResult = mrYes then
    ShowMessage('yes')
  else
    ShowMessage('no')
end;

Even in this case, it is quite simple, but let's go deeper with the configuration. Let's say that we need a TaskDialog similar to the following screenshot:

Fig. 12.1 The TTaskDialog component is configured to show three radio buttons

Using the plain API is not so simple to do this. So, let's see how to configure the component:

object tdRadioButtons: TTaskDialog
  Caption = 'The question'
  DefaultButton = tcbYes
  ExpandButtonCaption = 'More information'
  ExpandedText = 
    'Yes, you have to decide something about this question... ' + 'but I cannot help you a lot'
  Flags = [tfUseHiconMain, tfUseHiconFooter, tfVerificationFlagChecked]
  FooterIcon = 4
  FooterText = 'This is an important question...'
  RadioButtons = <
    item
      Caption = 'Yes, I want to buy this book'
    end
    item
      Caption = 'No, this book is awful'
    end
    item
      Caption = 'Maybe in the future'
    end>
  Text = 'Do you wanna buy "The Tragedy of Hamlet"?'
  Title = 'William ask:'
end

The preceding block of code contains the definition for the three radio buttons. The following code shows the dialog and the retrieval of the result:

procedure TMainForm.btnRadioClick(Sender: TObject);
begin
  tdRadioButtons.Execute;
  if tdRadioButtons.ModalResult = mrOk then
    ShowMessage('Selected radio button ' + tdRadioButtons.RadioButton.ID.ToString);
end;

Even in this case, we have defined the properties at design time so that the runtime code is quite simple. Just note that the user choice is stored in the RadioButton.ID property.

The TTaskDialog.Flags property can greatly change the behavior of the dialog. Here's the meaning of each element of its set:

Flag set element name

If set…

tfEnableHyperlinks

Content, footer, and expanded text can include hyperlinks

tfUseHiconMain

Uses the custom main icon

tfUseHiconFooter

Uses the custom footer icon

tfAllowDialogCancellation

Permits Task Dialog to be closed in the absence of a Cancel button

tfUseCommandLinks

Buttons are displayed as command links using a standard dialog glyph

tfUseCommandLinksNoIcon

Buttons are displayed as command links without a glyph

tfExpandFooterArea

Displays expanded text in the footer

tfExpandedByDefault

Expanded text is displayed when the Task Dialog opens

tfVerificationFlagChecked

The verification checkbox is initially checked

tfShowProgressBar

Displays the progress bar

tfShowMarqueeProgressBar

Displays the marquee progress bar

tfCallbackTimer

Callback Dialogs will be called every 200 milliseconds

tfPositionRelativeToWindow

Task Dialog is centered with respect to the parent window

tfRtlLayout

Text reads right to left

tfNoDefaultRadioButton

There is no default radio button

tfCanBeMinimized

The Task Dialog can be minimized

The real power of TaskDialogs comes when you build your dialog at runtime. Let's check what the fourth button does under the hood:

procedure TMainForm.btnConfirmClick(Sender: TObject);
var
  LFileName: string;
  LGSearch: String;
const
  GOOGLE_SEARCH = 99;
begin
  LFileName := 'MyCoolProgram.exe';
  tdConfirm.Buttons.Clear;
  tdConfirm.Title := 'Confirm Removal';
  tdConfirm.Caption := 'My fantastic folder';
  tdConfirm.Text :=
    Format('Are you sure that you want to remove ' + 'the file named "%s"?', [LFileName]);
  tdConfirm.CommonButtons := [];
  with TTaskDialogButtonItem(tdConfirm.Buttons.Add) do
  begin
    Caption := 'Remove';
    CommandLinkHint := Format('Delete file %s from the folder.', [LFileName]);
    ModalResult := mrYes;
  end;
  with TTaskDialogButtonItem(tdConfirm.Buttons.Add) do
  begin
    Caption := 'Keep';
    CommandLinkHint := 'Keep the file in the folder.';
    ModalResult := mrNo;
  end;

  if TPath.GetExtension(LFileName).ToLower.Equals('.exe') then
  begin
    with TTaskDialogButtonItem(tdConfirm.Buttons.Add) do
    begin
      Caption := 'Google search';
      CommandLinkHint := 'Let''s Google tell us what ' + 'this program is.';
      ModalResult := GOOGLE_SEARCH;
    end;
  end;

  tdConfirm.Flags := [tfUseCommandLinks];
  tdConfirm.MainIcon := tdiInformation;

  if tdConfirm.Execute then
  begin
    case tdConfirm.ModalResult of
      mrYes:
        ShowMessage('Deleted');
      mrNo:
        ShowMessage(LFileName + 'has been preserved');
      GOOGLE_SEARCH:
        begin
          LGSearch := Format('https://www.google.it/#q=%s', [LFileName]);
          ShellExecute(0, 'open', PChar(LGSearch), nil, nil, SW_SHOWNORMAL);
        end;
    end; //case
  end; //if
end;

It seems like a lot of code, but it is simple and can be easily parameterized and reused inside your program. The resultant dialog is as shown:

Fig. 12.3 The dialog customized by code

The third choice allows the user to search on Google about the program executable name. This is not a common choice in the MessageDlg dialog where buttons are predefined, but using the Task Dialog you can even ask something "strange" to the user (such as "do you want to ask Google about it?")

To achieve a better apparent speed, progress bars are great! The Task Dialog API provides a simple way to use progress bars inside dialogs. The classic Delphi solution relays a custom form with a progress bar and some labels (just like the "Compiling" dialog that you see when you compile a program within the Delphi IDE). However, in some cases, you need some simple stuff done and a Task Dialog is enough. If TTaskDialog has the tfCallbackTimer flag and tfShowProgressBar, the OnTimer event will be called every 200 milliseconds (five times a second), and the dialog will show a progress dialog that you can update within the OnTimer event handler. However, the OnTimer event handler runs in the main thread so that all the related advice applies (if the UI becomes unresponsive, consider a proper background thread and a queue to send information to the main thread).

This is the design time configuration of TTaskDialog tdProgress:

object tdProgress: TTaskDialog
  Caption = 'Please wait'
  CommonButtons = [tcbCancel]
  ExpandButtonCaption = 'More'
  ExpandedText = 
    'A prime number (or a prime) is a natural number greater'+' than 1 that has no positive divisors other than 1 ' + 'and itself.'
  Flags = [tfAllowDialogCancellation, tfShowProgressBar, 
tfCallbackTimer]
  FooterIcon = 3
  FooterText = 'Please wait while we are calculate prime numbers'
  Text = 'Let'#39's calculate prime numbers up to 1000'
  Title = 'Calculating prime numbers...'
  VerificationText = 'Remember my choice'
  OnButtonClicked = tdProgressButtonClicked
  OnTimer = tdProgressTimer
end

There are two event handlers, one to handle click on the Cancel button inside the dialog and one to handle the callback:

const
  MAX_NUMBERS = 1000;
  NUMBERS_IN_A_SINGLE_STEP = 50;

procedure TMainForm.tdProgressButtonClicked(Sender: TObject;
  ModalResult: TModalResult; var CanClose: Boolean);
begin
  if not FFinished then
  begin
    tdProgress.OnTimer := nil;
    ShowMessage('Calculation aborted by user');
    CanClose := True;
  end;
end;

procedure TMainForm.tdProgressTimer(Sender: TObject; TickCount: Cardinal;
  var Reset: Boolean);
var
  I: Integer;
begin
  for I := 1 to NUMBERS_IN_A_SINGLE_STEP do
  begin
    if IsPrimeNumber(FCurrNumber) then
      Inc(FPrimeNumbersCount);
    tdProgress.ProgressBar.Position := FCurrNumber * 100 div MAX_NUMBERS;
    Inc(FCurrNumber);
  end;

  FFinished := FCurrNumber >= MAX_NUMBERS;
  if FFinished then
  begin
    tdProgress.OnTimer := nil;
    tdProgress.ProgressBar.Position := 100;
    ShowMessage('There are ' + FPrimeNumbersCount.ToString + ' prime numbers up to ' + MAX_NUMBERS.ToString);
  end;
end;

To not block the main thread, the prime numbers are calculated a few at a time. When the calculation is ended, the callback is disabled by setting the OnTimer event handler to nil.

In other words, the real calculation is done in the main thread, so you should slice your process in to smaller parts so that it can be executed one (small) piece at time.

The following code fires the progress Task Dialog:

procedure TMainForm.btnProgressClick(Sender: TObject);
begin
  FCurrNumber := 1;
  FFinished := False;
  FPrimeNumbersCount := 0;
  tdProgress.ProgressBar.Position := 0;
  tdProgress.OnTimer := tdProgressTimer;
  tdProgress.Execute;
end;

Here's the resultant dialog:

Fig. 12.4 The Task Dialog with an embedded Progress Bar

There's more…

The new Task Dialog API can give your application a fresh breath, but that comes with cost because it works only on Vista or better, with enabled themes. So, how to work around the problem if you need to run the application also in Windows XP or in machine without themes enabled? For button 6, there's a simple code to check whether you can safely use the TTaskDialog component or whether you have to come back to normal ShowMessage or MessageDlg. Here's the event handler for the button 6:

procedure TMainForm.btnCheckWinVerClick(Sender: TObject);
var
  LTaskDialog: TTaskDialog;
begin
  if (Win32MajorVersion >= 6) and ThemeServices.ThemesEnabled then
  begin
    LTaskDialog := TTaskDialog.Create(Self);
    try
      LTaskDialog.Caption := 'MY Fantastic Application';
      LTaskDialog.Title := 'The Cook Task Dialog!';
      LTaskDialog.Text :=
        'This is a Task Dialog, so I''m on Vista ' + 'or better with themes enabled';
      LTaskDialog.CommonButtons := [tcbOk];
      LTaskDialog.Execute;
    finally
      LTaskDialog.Free;
    end
  end
  else
  begin
    ShowMessage('This is an old and boring ShowMEssage, ' + 'here only to support old Microsoft Windows OS ' + '(XP and below)');
  end;
end;

Try to disable the themes for your application and click on button 6.

Obviously, it is strongly suggested that you wrap this code in a function so that you do not have to write the same check code repeatedly.

Tip

Downloading the example code

You can download the example code files for this book from your account at http://www.packtpub.com. If you purchased this book elsewhere, you can visit http://www.packtpub.com/support and register to have the files e-mailed directly to you.

You can download the code files by following these steps:

  • Log in or register to our website using your e-mail address and password.

  • Hover the mouse pointer on the SUPPORT tab at the top.

  • Click on Code Downloads & Errata.

  • Enter the name of the book in the Search box.

  • Select the book for which you're looking to download the code files.

  • Choose from the drop-down menu where you purchased this book from.

  • Click on Code Download.

You can also download the code files by clicking on the Code Files button on the book's webpage at the Packt Publishing website. This page can be accessed by entering the book's name in the Search box. Please note that you need to be logged in to your Packt account.

Once the file is downloaded, please make sure that you unzip or extract the folder using the latest version of:

  • WinRAR / 7-Zip for Windows

  • Zipeg / iZip / UnRarX for Mac

  • 7-Zip / PeaZip for Linux

The code bundle for the book is also hosted on GitHub at https://github.com/PacktPublishing/Delphi-Cookbook-Second-Edition. We also have other code bundles from our rich catalog of books and videos available at https://github.com/PacktPublishing/. Check them out!

About the Author

  • Daniele Teti

    Daniele Teti is a software architect, trainer, and consultant with over 20 years of experience. He drives the development of the most popular Delphi open source project on GitHub—DelphiMVCFramework. He's also a huge fan of design patterns, machine learning, and AI. Daniele is the CEO of BIT Time Professionals, an Italian company specializing in high-level consultancy, training, development, and machine learning systems.

    Browse publications by this author

Latest Reviews

(19 reviews total)
mise à jour du livre précédent qui était très bon. détailler les nouvautés
Just what we needed at good price.
Did not receive my book........

Recommended For You

Book Title
Access this book, plus 7,500 other titles for FREE
Access now