May 1997 • Vol. 3 No. 5 US $7.50

Tips and techniques for Delphi

Better borders for MDI windows

R

ecently, we showed how you can make the edge of a Delphi MDI application’s parent window visible and improve its appearance (“Displaying the Correct Border-Style for MDI Parent Windows,” February 1997). However, you can improve on this technique in several ways and at the same time accommodate some common user-interface enhancements. In this article, we’ll show you several improvements to the technique we presented in the February issue. In addition, we’ll demonstrate some MDI parent-form enhancements that the new technique makes possible.

Backgrounds in MDI Windows,” in the March 1996 issue.) To provide access to the client window, Delphi defines the ClientHandle property for MDI parent forms. By using this property, you can adjust the appearance of the region of the MDI parent window that contains the MDI client windows. As you’ve probably noticed, Delphi automatically adjusts the client window to prevent MDI child windows from covering menu bars or hint panels in the MDI parent

1.0

Figure A

Figure B

By default, the border of an MDI parent window will look odd when the window clips its child windows.

If you apply the style WS_EX_CLIENTEDGE to the parent window, its border’s appearance improves.

2.0

One for the border In the article we mentioned above, we addressed the problem that occurs when you create one or more MDI child windows in a Delphi MDI application: Typically, the parent window clips one of the child windows, as shown in Figure A. The improved version appears in Figure B. Although Figure B does show a betterlooking MDI parent window, you’ll notice that a new problem has arisen. The sunken border that surrounds the main MDI region (where the child windows appear) also appears above the toolbar and (although you can’t see it in Figure B) below the hint panel at the bottom of the window. This problem occurs because we applied the WS_EX_CLIENTEDGE window style to the parent form’s window. In fact, we should’ve applied this window style to the MDI parent form’s client window. (If you’re unfamiliar with the structure of a Delphi MDI parent form, that’s OK, because it’s not well-documented. Every MDI parent form contains a secondary window that’s responsible for handling the visual appearance and location of the MDI child windows. For more information on the client window, see “Displaying

IN THIS ISSUE

1 3 4 7 9 10 18

Better borders for MDI windows Using TStringList objects to implement key/value dictionaries Displaying taskbar icons for secondary forms Use Panel components to simplify resizing Changing alignment precedence for a panel Easy environment variables Abbreviating ListBox entries

A Publication of The Cobb Group

window. As a result, we can apply the WS_EX_CLIENTEDGE window style to the client window and then let Delphi resize the window for us as necessary.

“Go long for a window” To adjust the border of the main form we created in our February article, we used the CreateParams method, since overriding that method gave us easy access to the existing window styles and a simple way to modify them. However, we can use a much simpler approach to adjust the appearance of the client window. Windows provides a function call named SetWindowLong() that allows you to change the style of a given window, based on the window’s handle and a window attribute you wish to modify. Here’s how we’ll use this function call to set the client window’s style: SetWindowLong(ClientHandle, GWL_EXSTYLE, WS_EX_CLIENTEDGE);

To make sure the client window has the correct appearance as soon as it’s visible, we’ll execute this function call in the main form’s OnCreate event handler. Unfortunately, merely calling the function SetWindowLong() won’t do the trick until we force Windows to change this window’s display configuration. The simplest way to do so is to call the SetWindowPosition() function with the client window’s window handle (along with some arbitrary position values). (The values we use are irrelevant, since Delphi will reset the window’s position based on the actual size of the MDI parent and any Panel components having an Align property value other than alNone. We suggest you use zero for the position values to help document that the values are unimportant.)

You can make the call using this value under Windows 3.1, but it won’t have any effect. (Of course, since the default window style under Windows 3.1 has a clearly defined inner edge, the call isn’t necessary.) However, even 16-bit applications that make this function call can take advantage of the new window style.

Doctoring MDI Now let’s create a simple example that demonstrates how you can modify the appearance of the MDI client window. To begin, create a basic MDI application using either the MDI Application project template or the Application Expert. (If you use the latter, make sure you set the main form’s FormStyle property to fsMDIForm and add another form to the project using the fsMDIChild form style.) In the main form’s FormCreate method, add the following lines: SetWindowLong(ClientHandle, GWL_EXSTYLE, WS_EX_CLIENTEDGE); SetWindowPos(ClientHandle, HWND_BOTTOM, 0, 0, 0, 0, SWP_NOCOPYBITS);

These lines will apply the correct style to the MDI client window. Next, add a Panel component to the left side of the main form, and change the Name property to NewPanel. Set the Panel component’s BevelOuter property to bvNone. (By eliminating the raised effect of the new Panel, you’ll make the edge of the client window more noticeable.) Change the Align property of the Panel to alLeft. Now, add two new menu items to the Window menu: Hide Panel and Show Panel. Double-click on the Hide Panel menu item and add the following code to its event handler:

If you’re using Delphi 1.0…

NewPanel.Hide;

If you want to use this technique with Delphi 1.0 applications, you’ll need to add the following line to the beginning of the implementation section:

Then, double-click on the Show Panel menu item and add the following code to its event handler: NewPanel.Show;

const WS_EX_CLIENTEDGE = 512;

This declaration is necessary because the WS_EX_CLIENTEDGE window style is meaningful only under Windows 95.

2

Delphi Developer’s Journal

Finally, build and run the application. When the application’s main form appears, you’ll immediately notice that the window displays the correct border style

for the client area and that the area no longer includes the status panel or menu bar. The form will resemble the one shown in Figure C. Now, select New from the File menu to create a child form. Move the child form to the left to force the parent form to clip the client form’s display. The appearance will be consistent with other MDI applications. Finally, choose Hide Panel from the Window menu to hide the new panel on the left side of the form, and choose Show Panel to restore it. As you make this change, you’ll notice that the client window changes size dynamically. Figure D shows the main form with the new panel hidden. Several commercial applications use a left-aligned area similar to the one we’ve created with the new panel. If you want to display such an area but don’t wish to use the technique we’ve shown here, you’ll need to rely on the panel component’s BevelInner and BevelOuter properties to identify the edges of this area.

Conclusion The client window of an MDI parent form is an important element of Delphi’s approach to MDI application design. By applying the WS_EX_CLIENTEDGE window style to the client window, you can ensure that your MDI applications will have a standard MDI appearance. ❖

Figure C

The MDI client border no longer appears above the menu bar or below the status panel.

Figure D

If you hide and show the new panel, you’ll notice that the client window resizes automatically.

Thanks to Rob Barlow of Milwaukee for sharing this tip with us. To show our appreciation, we’re sending Rob a check for $25.

VCL PROGRAMMING

Using TStringList objects to implement key/value dictionaries

D

ictionaries are among the most consistently popular books purchased year after year. This is no surprise, since dictionaries are invaluable when you need to find the definition of an unfamiliar word. Traditionally, if you know how to spell a given word, you’ll be able to locate it in a dictionary for that language, where you’ll find detailed information

2.0

about the word’s derivation, meaning, and usage. For each word, there’s a unique entry, although that entry may contain alternative meanings for different contexts. Likewise, in many applications, you’ll use a database to retrieve information that closely resembles the words and definitions in a dictionary. However, using such a database may be overkill for simple key/

May 1997

3

The TStringList class contains many methods, fields, and properties, but one of the least known is the Values property. The Values property allows you to access strings within a TStringList object. For you to access the strings via the Values property, they must conform to a format that’s remarkably similar to the entries in an initialization, or INI, file such as WIN.INI. That is, the key string and the value string must appear on either side of an equal sign, as shown below:

However, if you examine each of the strings that the TestString object contains, you’ll find that it doesn’t store the keys and values separately; rather it stores them in the same string. The Values property is basically a sophisticated mechanism for retrieving the second part of an INI-file-formatted string based on the first part. As a result, you can’t simply use the Find() method to locate a key or value, because that method requires the entire string as its search parameter. In addition to using the Values property to retrieve the second portion of INI-fileformatted strings, you can use it to add new key/value strings to a TStringList object. To do so, you specify the new key as an array index and then assign a text string to that index to set the value, as shown here:

This is the key=And this is the value

AString.Values[‘TheKey’] := ‘The Value’;

If a TStringList object named TestString contains the string above, you can write

As you can see, the Values property acts like a text-based array index for setting or retrieving a value string based on a key string.

value pairs. In this article, we’ll show how you can use TStringList objects to implement simple key/value dictionaries for pairs of strings.

Property “Values”

Label1.Caption := TestString.Values[‘This is the key’];

This statement will copy the value portion of the text string And this is the value

to the Caption property of Label1.

Manipulate INI files As we mentioned above, if the format of the strings in a TStringList object conforms to the INI-file format, you can supply a key string to retrieve the corresponding value string. Not coincidentally, the TIniFile class contains the ReadSectionValues method,

Displaying taskbar icons for secondary forms

I

f you’ve ever run Delphi 1.0 under Windows 95, you may have noticed something peculiar: Icons for the Object Inspector, the code editing window, and the form window appear on the taskbar until you minimize the main Delphi window. In fact, for any Delphi 1.0 application, icons for secondary forms (forms other than the main form) appear on the taskbar unless the user minimizes the main form. Then, if you minimize a secondary form, you can quickly restore it by clicking on its taskbar icon. In contrast, Delphi 2.0 doesn’t display taskbar icons for the Object Inspector, code editing window, or form window. If you minimize any of those windows, they appear as minimized window captions at the

4

Delphi Developer’s Journal

bottom of the screen. Likewise, applications you create in Delphi 2.0 won’t display taskbar icons for secondary forms. If you want to display taskbar icons for secondary forms in your Delphi 2.0 application, you’ll first need to apply the WS_EX_APPWINDOW style to those forms. One of the easiest ways to do this is to create an overridden version of the CreateParams() method that applies this style to the secondary form’s window. For example, create a blank-form application, and then add a new form. Save the main form’s unit as MAIN.PAS, the second form’s unit as SECOND.PAS, and the project as TASKICON.DPR. In the implementation section of the main form’s source file, add the following line:

which initializes an external TStringList with the entries from a given section in an INI file. Once you’ve initialized an external TStringList object this way, you can retrieve the individual value strings by supplying one of the corresponding key strings. Conversely, you can transfer modified key/ value strings back to the TIniFile object, and the strings will be in the correct format.

Key values and cherished associations Now let’s put these techniques to work in a simple example. To begin, create a blank form project and place on the main form two Edit components, two Label components, and a ScrollBar component. Arrange the components and resize the form to resemble the one shown in Figure A. Next, clear the Text property of the two Edit components. When the program runs, we’ll use the Edit components to set keys and values in a TStringList object, and the default text will automatically add entries to the list. At this point, add the following event handlers for these components: OnChange for Edit1, OnExit for Edit2, OnScroll for ScrollBar1, and OnCreate for the main form. These events will initialize and up-

uses Second;

Then, place a Button component on the main form, double-click it, and add the line Form2.Show;

to the Button component’s OnClick event handler. In the SECOND.PAS source file, add these lines to the form’s class declaration: procedure CreateParams(var Params: TCreateParams); override;

Then, to the implementation section of that unit, add the following: procedure TForm2.CreateParams(var Params: TCreateParams); begin inherited CreateParams(Params);

Figure A

Arrange the components to resemble this form.

date the TStringList object as you modify them at runtime. Finally, modify the source code for the main form according to Listing A on the next page. In particular, make sure you complete the following tasks: • Add the UpdateViews procedure’s declaration along with the MyStrings and idx data members to the private section of the form’s class declaration. • Add the uses IniFiles statement to the unit’s implementation section. • Enter the entire UpdateViews procedure. (We’ve highlighted in red the code you’ll need to enter.) After you finish entering the code, save the project as LOOKUP.DPR and the main form’s source file as KEY_VAL.PAS. Build and run the application.

Params.ExStyle := Params.ExStyle or WS_EX_APPWINDOW; end;

When you finish adding this code, build and run the application. As soon as the main form appears, click Button1. When the secondary form appears, you’ll notice that a second icon appears on the taskbar. Click the secondary form’s Minimize button, and you’ll see the form minimize to the taskbar icon, as shown in Figure A. Figure A

You can force Windows to create taskbar icons for secondary forms.

May 1997

5

When the main form appears, enter the key string The Association in the first edit field, and enter the value string Cherish is the word I use to describe… in the second. When you move the focus back to the first edit field, you’ll notice that that several things happen: The Index changes from -1 to 0; the Key/ Value string contains The Association=Cherish

is the word I use to describe…; and the scrollbar moves to the far right. Add several more artists and song titles as key and value strings, and notice that the index for each one increments appropriately. Once you’ve entered several strings, enter The Association again in the first edit field. As soon as the text appears in the field, notice

Listing A: KEY_VAL.PAS unit key_val; interface uses Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs, StdCtrls; type TForm1 = class(TForm) Edit2: TEdit; Edit1: TEdit; Label1: TLabel; Label2: TLabel; ScrollBar1: TScrollBar; procedure FormCreate(Sender: TObject); procedure Edit1Change(Sender: TObject); procedure Edit2Exit(Sender: TObject); procedure ScrollBar1Scroll(Sender: TObject; ScrollCode: TScrollCode; var ScrollPos: Integer); private { Private declarations } MyStrings : TStringList; idx : Integer; procedure UpdateViews; public { Public declarations } end; var Form1: TForm1; implementation {$R *.DFM} uses IniFiles; procedure TForm1.UpdateViews; begin ScrollBar1.Max := MyStrings.Count - 1; ScrollBar1.Position := idx; Label1.Caption := ‘Index = ‘ + IntToStr(idx); if idx >= 0 then Label2.Caption := ‘Key/Value (‘ + MyStrings[idx] + ‘)’

6

Delphi Developer’s Journal

else Label2.Caption := ‘Key/Value = ()’; end; procedure TForm1.FormCreate(Sender: TObject); var WinIni : TIniFile; begin ScrollBar1.Min := -1; idx := -1; MyStrings := TStringList.Create; WinIni := TIniFile.Create(‘WIN.INI’); {WinIni.ReadSectionValues(‘Extensions’, MyStrings);} WinIni.Destroy; UpdateViews; end; procedure TForm1.Edit1Change(Sender: TObject); begin Edit2.Text := MyStrings.Values[Edit1.Text]; idx := MyStrings.IndexOf(Edit1.Text + ‘=’ + Edit2.Text); UpdateViews; end; procedure TForm1.Edit2Exit(Sender: TObject); begin MyStrings.Values[Edit1.Text] := Edit2.Text; idx := MyStrings.IndexOf(Edit1.Text + ‘=’ + Edit2.Text); UpdateViews; end; procedure TForm1.ScrollBar1Scroll(Sender: TObject; ScrollCode: TScrollCode; var ScrollPos: Integer); begin idx := ScrollBar1.Position; Edit1.Text := ‘’; Edit2.Text := ‘’; UpdateViews; end; end.

that the text string Cherish is the word I use to describe… appears in the second field. Exit the program, and remove the comment braces to enable the line

Figure B

WinIni.ReadSectionValues(‘Extensions’, MyStrings);

in the FormCreate method. Rebuild and run the application. Enter txt in the first edit field, and confirm that the second edit field contains the path to the default text file editor, typically NOTEPAD.EXE. As you use the first field to enter the extensions associated with other applications that store their extension settings in the WIN.INI file, the path and filename for the application will appear in the second field, as shown in Figure B.

You can use the TStringList class’s Values property to perform simple key/value lookup operations.

Conclusion Many applications contain simple key/ value tables. If you wish to store only a few key/value strings and don’t need the overhead of a database file, you can use the TStringList class to help you store and retrieve that data. ❖

VCL BASICS

Use Panel components to simplify resizing

I

f you’ve used Delphi for any significant project, you’ve probably noticed the Align property of many visual components. However, you may not have realized how useful this property can be. For many applications, you’ll want to create on a form some static regions that maintain relative position as the user resizes the form. It’s common for beginning Delphi programmers to perform resizing tasks using the OnResize event, moving or resizing individual controls as needed. However, you can use Panel components to simplify this process and eliminate much of the tedious resizing logic. In this article, we’ll show how you can use a Panel component’s Align property to handle many of these tasks—with little or no code.

Edge alignment versus client alignment If you examine the possible values for the Align property, you’ll find alBottom, alClient, alLeft, alNone (the default value), alRight, and alTop. As you could guess,

alNone specifies no automatic alignment; the Panel component’s size and position will be a result of changes you make. The remaining values fall into two categories: edge and client alignment values. The alClient property value specifies that Delphi should resize and position the control so that it covers the entire client region of the parent window. In contrast, the alBottom, alLeft, alRight, and alTop values specify an edge of the client region that the control will adhere to. For example, if you specify a value of alTop for a control’s Align property, the control will maintain the design-time height, but Delphi will adjust the control’s width to match the form’s client area width.

1.0

2.0

This article is based on a submission from Andre v.d Merwe. Andre works as a professional Delphi programmer in South Africa. You can contact him via the Internet at [email protected]

Panels as owners A Panel component is one on which you can place other components. Any components that you place on a panel will then recognize the panel as their owner. The panel is responsible for initializing and destroying the components it owns.

May 1997

7

In addition, the position of the components on the panel will be relative to the upper-right corner of the panel itself. For instance, if you place a Panel component on a form and then place a Button component on the panel, moving the panel will also move the button. If you combine the characteristics of Panel components with the setting of the Align property to various values, you’ll notice that you can use these features in tandem. However, Delphi specifies the following default order of evaluation for alignment options: • alBottom and alTop • alLeft and alRight • alClient This default order means that if you place two Panel components on a form and set one’s Align property to alTop and the other’s to alLeft, the one set for alTop will always appear above the alLeft one—no matter which component you place first! (Fortunately, you have an alternative if you need to specify a component whose alignment requires nonstandard priority. For more information, see “Changing Alignment Precedence for a Panel.”)

Problems with MDI forms For Single Document Interface (SDI) applications, the above technique works well. However, there are limits to how you can use this technique for a Multiple Document Interface (MDI) application. Specifically, if you cover the main form entirely with Panel components, you won’t be able to see the MDI child forms because the position of the child forms is relative to the main form area that’s not covered by Panel components. For these situations, you’ll need to use the technique we describe in “Changing Alignment Precedence for a Panel.”

Conclusion You may not always be able to use code in an OnResize event handler to correctly modify component positions and size. However, by using combinations of Panel components and Align property settings, you can greatly simplify the task of sizing and positioning many form elements. ❖

Figure A

Panel delivery Now let’s create an example that demonstrates how you can use Panel components to manage the placement of components on complex forms. To begin, create a blankform project and place two Panel components on it. Set the Align property of the first panel to alLeft and its Width property to 100. Set the Align property of the second panel to alClient. Next, place a third Panel component on the second, and set its Align property to alTop and its Height property to 50. Finally, place a Memo component below the third panel (but on the second), and set its Align property to alClient. Now, build and run the application. When the main form appears, you’ll notice that resizing the form doesn’t affect the location of the Memo component’s upperright corner, as shown in Figures A and B.

8

Delphi Developer’s Journal

You can use Panel components…

Figure B

…to control the position of other components relative to each other.

ADVANCED WINDOWS PROGRAMMING

Changing alignment precedence for a panel

I

n the previous article, we describe how you can use Panel components to automate the sizing and positioning of other components (“Use Panel Components to Simplify Resizing”). However, you may be wondering how to change the way Delphi determines which component in a set of components to align first. In this article, we’ll show how you can use the TWinControl class’s AlignControls method to alter the order of alignment evaluation. Since this ordering problem occurs primarily with Multiple Document Interface (MDI) forms, we’ll explore how you can apply this technique to those forms in particular.

MDI problems As we mention in the previous article, you can’t use the same techniques to solve problems with panel arrangements in an MDI form that you use for Single Document Interface (SDI) forms. The primary reason is that Delphi automatically adjusts the size of an MDI application’s client window region as you add status panels or a menu bar. Unfortunately, if you place a panel component on an MDI parent form and set its Align property to alClient, that Panel will obscure any MDI child forms that you create. Therefore, you can’t use combinations of Panel components that own other Panel components to circumvent Delphi’s default alignment strategy.

Aligning controls your way The secret to changing the alignment ordering for a group of components is the TWinControl class’s AlignControls method. By default, Delphi calls this method anytime it needs to adjust the alignment of child controls for a parent window. In turn, this method examines the Align properties of each component and adjusts the size of each one based on the default ordering rules. For example, after resizing a form, Delphi needs to adjust the size of each Panel component whose Align property specifies something other than alNone. The AlignControls method allows the form to adjust the size of each of the child controls according to its Align property settings.

Fortunately, AlignControls is a virtual method, which means you can customize its behavior quite easily by creating an overridden version of the method. Once inside this method, you should first call the inherited version of AlignControls. Doing so aligns all the components using the standard ordering. After making this call, you can then change the alignment of any panel simply by adjusting its position. If you override the AlignControls method to adjust the size of Panel components on an MDI parent form, you’ll need to manually adjust the size and position of the MDI client window as well. (This is a borderless window that specifies the region that will contain MDI child forms.) The best way to make this adjustment is to call the SetWindowPos() Windows function, as in

1.0

2.0

Peter Laman provided much of the material for this tip. You can contact Peter via the Internet at [email protected]

SetWindowPos(ClientHandle, HWND_BOTTOM, NewLeft, NewTop, NewWidth, NewHeight, SWP_NOCOPYBITS);

In this call, the HWND_BOTTOM parameter specifies that the client window will appear at the bottom of the layers of windows, and the SWP_NOCOPYBITS value helps resolve some problems that occur when you use the MDI client for OLE toolbar docking.

Setting precedence Let’s create an MDI application that displays two Panel components but uses nonstandard precedence rules in aligning the panels. Begin by using the MDI Application project template or the Application Expert to build a new MDI project that displays a menu bar, a toolbar, and a status line. Place a new Panel component on the left side of the main form. Set the Panel component’s Align property to alLeft and its Width property to 100. In the private section of the TMainForm class declaration, add the following method declaration: procedure AlignControls(AControl: TControl; var Rect: TRect); override;

Next, add the following code to the main form’s implementation section:

May 1997

9

procedure TMainForm.AlignControls(AControl: TControl; var Rect: TRect); begin inherited AlignControls(AControl, Rect);

Figure A

Panel1.Top := 0; Panel1.Height := Panel1.Height + SpeedPanel.Height; SpeedPanel.SetBounds(Panel1.Width, 0, SpeedPanel.Width - Panel1.Width, SpeedPanel.Height); SetWindowPos(ClientHandle, HWND_BOTTOM, Panel1.Width, SpeedPanel.Height, SpeedPanel.Width, Panel1.Height - SpeedPanel.Height, SWP_NOCOPYBITS); end;

After you finish entering the code, build and run the application. When the main form appears, you’ll notice that the panel containing the speedbuttons aligns to the right of the new panel that we added to the project, as shown in Figure A.

You can override the AlignControls method in order to customize the alignment order.

Conclusion Using Panel components and Align properties, you can create complex arrangements. However, if you want to use ordering other than the default, consider overriding the AlignControls method. ❖

WINDOWS API PROGRAMMING

1.0

2.0

Easy environment variables by Ray Lischner

Y

ou’re probably familiar with environment variables. You use them in AUTOEXEC.BAT files to set paths, sound card parameters, and so on. If you write Common Gateway Interface (CGI) programs to automate web server actions, you need to access environment variables to transfer vital information to and from the server. Even DOS and Windows use the PATH statement to locate executable files. With so many different uses for these variables, it’s unfortunate that Delphi lacks convenient access to them. This drawback is easy to remedy, however. One of the difficulties in manipulating environment variables is that Windows 3.1 and Win 32 (Windows 95 and Windows NT) provide entirely different mechanisms to access and use them. This means Delphi 1 and Delphi 2 use completely different functions for accessing environment variables. With a

10

Delphi Developer’s Journal

little extra work, however, you can write a class that has the same interface in Delphi 1 and in Delphi 2. The class uses different implementations for the different versions of Delphi, but your applications can freely read and write environment variables in both Delphi 1 and Delphi 2. This article introduces you to some of the tricky aspects of using environment variables in Delphi programs. In particular, I’ll show you how to use the TEnvironment class to safely read and write these values.

Variable environments An environment variable is a means of communicating static information, such as configuration data for an application, between programs. For instance, you may be familiar with environment variables from altering a system’s CONFIG.SYS and AUTOEXEC.BAT files. An application or device driver might

require you to specify directories, IRQ settings, or other configuration data as the values of environment variables. The most commonly used environment variable is PATH. The value of the PATH environment variable tells DOS and Windows where to look for applications and related files. For instance, Figure A shows several environment variables in an AUTOEXEC.BAT file. The first statement sets the PATH variable to search multiple directories; the next ones set a few variables to define configuration parameters for a sound card. Figure A SET SET SET SET

PATH=C:\WINDOWS;C:\DOS;D:\USER\MYAPPS BLASTER=A220 D1 I5 H5 P330 T6 SOUND=C:\SB16 MIDI=SYNTH:1 MAP:E

These variables are typical environment variables you might find in the AUTOEXEC.BAT file.

Environment variables have gained a new prominence with the phenomenal growth in popularity of web servers and CGI programs. You can write a CGI program in Delphi, but in order to do so, you must access environment variables. The web server communicates with the CGI program via environment variables such as REQUEST_METHOD, whose value is the string GET or POST, depending on the method of a form request. Environment variables are especially important when you launch applications. Sometimes, you’ll want to change an environment variable (especially PATH) before launching a child application. To do so, the parent application can specify a block of environment variables when launching the child application. The block contains the names and values of every environment variable. Typically, DOS and Windows share a single, global environment block from which every application gets its environment variables. If you want to launch an application with different environment variables, then you must create a new environment block and set the desired variables in that block. You can then launch the application with the new environment block. Unfortunately, the details of how to create an environment block, set environment vari-

ables, and launch applications change between Delphi 1.0 and Delphi 2.0. More specifically, the Windows API changes from Windows 3.x to the Win32 API. To insulate the programmer from these differences, we’ll define the ENVIRON unit and the TEnvironment class. This class will simplify access to environment variables in a Delphi program.

Windows 3.1 API Windows 3.1 uses the same environment block that DOS defines. If your application changes any environment variable, every other application sees that change. The environment block’s size is fixed, so a careless change might crash your application or even Windows itself. To specify new environment variables for a child program, your parent program must create an entirely new environment block where you can set any environment variables you desire. Typically, you’d copy most of the variables from the global environment and change just a few variables or add some new ones. You launch the child by calling LoadModule and passing the handle of the environment block as one of the parameters. The difficulty is in the need for the parent and child applications to agree on when it’s safe to free the new environment block. The simplest solution is for the parent application to wait until the child finishes. Then the parent can free the environment block. An application accesses its environment block by calling the Windows API routine GetDosEnvironment. This function returns a pointer to a character array. The character array contains every environment variable and its value as zero-terminated strings. To mark the end of the environment block, Windows adds an extra zero byte after the last name/value pair in the string. Figure B shows the structure of a sample environment block, using the syntax for a Pascal string. Figure B ‘PATH=C:\WINDOWS;C:\DOS;D:\USER\MYAPPS’#0 ‘BLASTER=A220 D1 I5 H5 P330 T6’#0 ‘SOUND=C:\SB16’#0 ‘MIDI=SYNTH:1 MAP:E’#0#0 This is the structure of an example Windows 3.1 environment block.

May 1997

11

Win32 API Most developers will agree that Windows 95 and Windows NT are much easier to work with than Windows 3.1 You can call new API functions to retrieve and, more important, change the value of an environment variable. When one application changes an environment variable, no other application sees the changed value. If the application launches a child program, Windows automatically passes the changed environment to the child process. To learn more about environment variables in Windows 95 and Windows NT, read the section describing GetEnvironmentString in the API documentation.

The ENVIRON unit In Delphi, you’re faced with two difficulties. The first is how to reconcile the differences between Delphi 1 (Windows 3.1) and Delphi 2 (Windows 95 and Windows NT). The second is how to define a simple class that’s easy to use. The ENVIRON unit declares the class TEnvironment and a single instance, named Env, which represents the global environment. This unit also initializes the instance and frees it when the application termi-

nates. To get the value of an environment variable, simply access the default array property, using the name of the environment variable, as in Env[‘PATH’]

This statement returns a string for the value of the environment variable; it returns an empty string if the variable isn’t defined. If you need to modify an environment variable, use the same Env reference in an assignment statement. The new value is invisible to other applications, even in Windows 3.1. Instead of modifying the existing environment block, the Env object creates a new one. You can access this environment block for use in a call to LoadModule by calling GetEnvBlock. To free the environment block, call FreeEnvBlock, passing the environment block as an argument. The environment block class is TEnvBlock, which has a different type definition in Delphi 1 (Word) and Delphi 2 (PChar). Listing A shows the ENVIRON unit. The TEnvironment class does the real work, so let’s take a closer look at some of its more interesting methods. Continued on page 15

Listing A: ENVIRON.PAS unit Environ; interface uses Classes; type TEnvBlock = {$ifdef WIN32} PChar {$else} Word {$endif}; TEnvironment = class(TPersistent) private fModified: Boolean; fStrings: TStringList; fEnvSize: Cardinal; {$ifndef WIN32} fEnvBlock: TEnvBlock; {$endif} function GetEnv(const EnvVar: string): string; procedure SetEnv(const EnvVar: string; const Value: string); function GetCount: Integer; function GetName(Index: Integer): string; function GetSorted: Boolean; procedure SetSorted(Sorted: Boolean);

12

Delphi Developer’s Journal

protected constructor CreateFromEnv(Env: PChar); virtual; procedure FreeStrings; virtual; procedure Initialize(Env: PChar); virtual; function GetValuePtr(Index: Integer): PString; procedure SetValuePtr(Index: Integer; Ptr: PString); public constructor Create; virtual; destructor Destroy; override; function AllocEnvBlock: TEnvBlock; virtual; procedure Assign(Source: TPersistent); override; property Count: Integer read GetCount; property Env[const EnvVar: string]: string read GetEnv write SetEnv; default; property Sorted: Boolean read GetSorted write SetSorted; property Names[Index: Integer]: string read GetName; procedure Delete(Index: Integer); virtual;

procedure Remove(const EnvVar: string); property Modified: Boolean read fModified; property Size: Cardinal read fEnvSize; end; { Global environment } var Env: TEnvironment; function GetEnvBlock: TEnvBlock; procedure FreeEnvBlock(EnvBlock: TEnvBlock); implementation uses SysUtils, WinTypes, WinProcs; {$ifndef WIN32} procedure SetLength(var Str: string; Length: Byte); begin Str[0] := Chr(Length) end; procedure SetString(var Str: string; Data: PChar; Length: Byte); begin Str[0] := Chr(Length); Move(Data^, Str[1], Length); end; {$endif} function GetEnvBlock: TEnvBlock; begin {$ifdef WIN32} Result := nil; {$else} Result := Env.AllocEnvBlock; {$endif} end; procedure FreeEnvBlock(EnvBlock: TEnvBlock); begin {$ifdef WIN32} if EnvBlock nil then HeapFree(GetProcessHeap, 0, EnvBlock); {$else} if EnvBlock 0 then if GlobalSize(EnvBlock) > 0 then begin GlobalFree(EnvBlock); if Env.fEnvBlock = EnvBlock then Env.fEnvBlock := 0; end; {$endif} end; constructor TEnvironment.Create; begin fStrings := TStringList.Create; inherited Create; end; constructor TEnvironment.CreateFromEnv(Env: PChar);

begin Create; Initialize(Env); {$ifndef WIN32} if LongRec(Env).Lo = 0 then fEnvBlock := LongRec(Env).Hi; {$endif} end; procedure TEnvironment.Assign(Source: TPersistent); var I: Integer; begin if Source is TEnvironment then begin fStrings. Assign(TEnvironment(Source).fStrings); fModified := True; {$ifndef WIN32} fEnvBlock := 0; {$endif} fEnvSize := 1; { trailing #0 } with fStrings do for I := 0 to Count-1 do fEnvSize := fEnvSize + Length(Strings[I]) + Length(GetValuePtr(I)^) + 2; end else inherited Assign(Source); end; destructor TEnvironment.Destroy; begin FreeStrings; fStrings.Free; inherited Destroy; end; function TEnvironment.GetSorted: Boolean; begin Result := fStrings.Sorted; end; procedure TEnvironment.SetSorted(Sorted: Boolean); begin if Sorted fStrings.Sorted then begin fModified := True; {$ifndef WIN32} fEnvBlock := 0; {$endif} end; fStrings.Sorted := Sorted; end; procedure TEnvironment.FreeStrings; var I: Integer; begin with fStrings do for I := 0 to Count-1 do begin DisposeStr(GetValuePtr(I));

Continued on page 14 May 1997

13

Continued from page 13 SetValuePtr(I, nil); end; end; procedure TEnvironment.Initialize(Env: PChar); var Name: string; ValStr: PString; Value: PChar; begin fStrings.Clear; fEnvSize := 1; { for the terminating zero byte } while Env[0] #0 do begin { Find the value, which comes after the equal sign. } Value := StrScan(Env, ‘=’); if Value = nil then begin { This should never happen, but it’s better to be safe. } Value := StrEnd(Env); Name := StrPas(Env); end else begin SetString(Name, Env, Value-Env); Inc(Value); end; ValStr := NewStr(StrPas(Value)); if Name ‘’ then begin fStrings.AddObject(Name, TObject(ValStr) ); fEnvSize := fEnvSize + Length(Name) + Length(ValStr^) + 2; end; { Skip to the end of the value, and skip over its #0 byte } Env := StrEnd(Value); Inc(Env); end end; function TEnvironment.AllocEnvBlock: TEnvBlock; var I: Integer; Ptr: PChar; begin {$ifdef WIN32} Result := HeapAlloc(GetProcessHeap, 0, Size); if Result = nil then OutOfMemoryError; Ptr := Result; {$else} if fEnvBlock 0 then begin Result := fEnvBlock; Exit; end;

14

Delphi Developer’s Journal

Result := GlobalAlloc(Gmem_Fixed or Gmem_Share, Size); if Result = 0 then OutOfMemoryError; Ptr := GlobalLock(Result); if Ptr = nil then begin GlobalFree(Result); OutOfMemoryError; end; {$endif} try for I := 0 to Count-1 do begin StrPCopy(Ptr, fStrings[I]); Inc(Ptr, Length(fStrings[I])); Ptr[0] := ‘=’; Inc(Ptr); StrPCopy(Ptr, GetValuePtr(I)^); Inc(Ptr, Length(GetValuePtr(I)^) + 1); end; Ptr[0] := #0; except {$ifdef WIN32} HeapFree(GetProcessHeap, 0, Result); {$else} GlobalUnlock(Result); GlobalFree(Result); {$endif} raise; end; {$ifndef WIN32} fEnvBlock := Result; {$endif} end; function TEnvironment.GetCount: Integer; begin Result := fStrings.Count end; function TEnvironment.GetName(Index: Integer): string; begin Result := fStrings.Strings[Index] end; function TEnvironment.GetEnv(const EnvVar: string): string; var Index: Integer; begin Index := fStrings.IndexOf(EnvVar); if Index < 0 then Result := ‘’ else Result := GetValuePtr(Index)^; end; function TEnvironment.GetValuePtr(Index: Integer): PString; begin

Result := PString(fStrings.Objects[Index]); end; procedure TEnvironment.SetValuePtr(Index: Integer; Ptr: PString); begin with fStrings do begin if (Ptr nil) and (Objects[Index] nil) and (PString(Objects[Index])^ Ptr^) then begin fModified := True; {$ifndef WIN32} fEnvBlock := 0; {$endif} end; Objects[Index] := TObject(Ptr); end; end; procedure TEnvironment.Delete(Index: Integer); begin if (Index >= 0) and (Index < fStrings.Count) then begin {$ifdef WIN32} if Self = Environ.Env then SetEnvironmentVariable(PChar (fStrings[Index]), nil); {$endif} fEnvSize := fEnvSize Length(fStrings[Index]) Length(GetValuePtr(Index)^) - 2; fStrings.Delete(Index); fModified := True; {$ifndef WIN32} fEnvBlock := 0; {$endif} end; end;

procedure TEnvironment.SetEnv(const EnvVar: string; const Value: string); var Index: Integer; OldVal: PString; begin Index := fStrings.IndexOf(EnvVar); if Index < 0 then begin { New environment variable } fStrings.AddObject(EnvVar, TObject(NewStr(Value))); fEnvSize := fEnvSize + Length(EnvVar) + Length(Value) + 2; end else if GetValuePtr(Index)^ = Value then { Same name, same value } Exit else begin { new value for an existing variable } OldVal := GetValuePtr(Index); fStrings.Objects[Index] := nil; fEnvSize := fEnvSize - Length(OldVal^); DisposeStr(OldVal); SetValuePtr(Index, NewStr(Value)); fEnvSize := fEnvSize + Length(Value); end; fModified := True; {$ifdef WIN32} if Self = Environ.Env then SetEnvironmentVariable(PChar(EnvVar), PChar(Value)); {$else} fEnvBlock := 0; {$endif} end; procedure Terminate; far; begin Env.Free end;

procedure TEnvironment.Remove(const EnvVar: string); var Index: Integer; begin Index := fStrings.IndexOf(EnvVar); if Index >= 0 then Delete(Index); end;

initialization AddExitProc(Terminate); {$ifdef WIN32} Env := TEnvironment. CreateFromEnv(GetEnvironmentStrings); {$else} Env := TEnvironment. CreateFromEnv(GetDOSEnvironment); {$endif} end.

Continued from page 12 The fStrings field stores the actual environment variables and their values. A string list can store an object with each string. In this case, the “object” is a pointer to another string—the value. You can decide whether or not you want to sort the list of environment variables. By default, the list is unsorted, preserving the order in the global

environment. If you decide to sort the list, you’ll find it takes less time to look up environment variables because Delphi can use binary search instead of linear search. The Initialize method parses the global environment, extracting all the variables and their values. Each pair of variables and values is joined by an equal sign and is stored as a null-terminated string.

May 1997

15

The string list (fStrings) does most of the hard work of looking up variables and values. The TEnvironment class merely delegates the work to methods such as GetCount, GetName, and GetEnv. The challenging part of the process happens when you change an environment variable. Before jumping in too deeply, take a step back and consider the general problem. Most of the time, applications won’t change any environment variables. In that case, GetEnvBlock returns nil (Delphi 2) or 0 (Delphi 1). You can pass this value to LoadModule, which tells Windows to use the global environment. If you change an environment variable and call GetEnvBlock, you want to get an environment block that you can use in a call to LoadModule. In Delphi 2, you can still use nil because Windows takes care of the details of passing the altered environment to the child process. In Delphi 1, however, you want to return the global handle of the new environment block. In other words, TEnvironment needs to remember whether any environment variable has changed. If so, it needs to allocate a new environment block in Delphi 1. To avoid allocating and freeing blocks needlessly, TEnvironment doesn’t actually allocate the new environment block until you call the AllocEnvBlock method. This method allocates a block and copies all the environment variables, using the Windows format of zero-terminated strings. Usually, you don’t call AllocEnvBlock directly but rather call the global function GetEnvBlock, which calls the method for you. In Delphi 2, you don’t need to allocate a new environment block so long as the modified environment represents the global environment. This means that the TEnvironment object must be the same object as the Env variable. If they refer to different objects, the TEnvironment object is not the global environment, so it must allocate a new environment block to pass to LoadModule or CreateProcess. Rarely will you ever need to create separate instances of TEnvironment, but in case you do, you can be sure that TEnvironment will always do the right thing. To illustrate how the TEnvironment object records changes to the environment, exam-

16

Delphi Developer’s Journal

ine the SetValuePtr method in Listing A, which is in the code segment highlighted in red on page 15. This method changes the value of an environment variable. It sets the fModified field to True and, in Delphi 1, clears the fEnvBlock field. This change forces the TEnvironment object to allocate a new environment block when AllocEnvBlock is later called.

WHICH hunt Theory can be interesting, but it’s no substitute for practical examples. The first example, shown in Listing B, demonstrates how you can use the Env variable to obtain the value of the PATH environment variable to locate a file. This trivial program works like the WHICH program (typically available on UNIX systems): If a file exists in any directory in the path, the program prints the full path to the file. You can use this tool to determine whether Windows can find a DLL or EXE file in your path and, if so, where the file resides. Listing B: WHICH.PAS program Which; uses SysUtils, Environ; var I: Integer; Path: string; begin Path := Env[‘PATH’]; for I := 1 to ParamCount do WriteLn(FileSearch(ParamStr(I), Path)); end.

Put the PATH behind you Next, let’s look at how you can modify an environment variable before launching an application. Many Java programs require the CLASSPATH environment variable. It specifies directories and files where the Java Virtual Machine (JVM) looks for class files. The problem is that individual Java tools and programs sometimes require incompatible Java classes. One simple solution is to write a program that sets the CLASSPATH environment variable uniquely for different Java programs. Windows 95 and Windows NT can customize environ-

ment variables for separate applications, but they don’t distinguish between the various Java classes that use the same JVM executable. The task, therefore, is to store a map between Java class name and environment information—decide which JVM executable to use and what value to assign to the CLASSPATH environment variable. The program looks up the appropriate information, sets the CLASSPATH variable, and uses the new environment block to launch the Java application. It waits until Java finishes and then frees the environment block. Listing C shows the relevant parts of this application. If the Java class isn’t registered, or if its ClassPath registry entry is missing or an empty string, the program doesn’t modify the environment. In that case, GetEnvBlock returns 0 or nil. If the Java class has a custom CLASSPATH, the program modifies the environment. In Delphi 1, GetEnvBlock returns a newly allocated block. In Delphi 2, it returns nil because Windows manages the modified environment. When you write the launcher application, you don’t need to worry about these details—just call GetEnvBlock and FreeEnvBlock and let the ENVIRON unit do the hard work for you.

Conclusion Many programming tasks require you to use environment variables. Delphi doesn’t have built-in support for environment variables, but you can define a class that makes them easy to work with. More important, you can create a class that has the same interface in Delphi 1 and Delphi 2, despite significant differences in the way environment variables work in their respective native operating systems. ❖ Ray Lischner is the author of Secrets of Delphi 2 (Waite Group Press, 1996), a book that reveals undocumented features of Delphi 1 and Delphi 2. He is a contributor to several Delphi periodicals and is a familiar figure on the Delphi Usenet newsgroups. Mr. Lischner is the founder and president of Tempest Software, which specializes in consulting and training for object-oriented languages, components, and tools. You can contact Ray via the Internet at [email protected]

Listing C: RUNJAVA.PAS program RunJava; uses SysUtils, Registry, Environ, Launch in ‘LAUNCH.PAS’; var JavaClass: string; { name of the Java class } Dir: string; { directory for the Java class } JvmPath: string; { complete path to the Java executable } ClassPath: string; { CLASSPATH environment variable } EnvBlock: TEnvBlock; { new environment block } Reg: TRegIniFile; begin if ParamCount 1 then raise Exception.CreateFmt(‘Usage: %s class-filename’, [ParamStr(0)]); { Get the JVM and CLASSPATH info from the registry. } JavaClass := ChangeFileExt( ExtractFileName(ParamStr(1)), ‘’); Dir := ExtractFilePath( ExpandFileName(ParamStr(1))); Reg := TRegIniFile.Create( ‘\Software\Tempest Software\RunJava’); try JvmPath := Reg.ReadString(JavaClass, ‘JVM’, ‘java.exe’); ClassPath := Reg.ReadString(JavaClass, ‘ClassPath’, ‘’); finally Reg.Free; end; { Empty string means don’t change the CLASSPATH. } if ClassPath ‘’ then Env[‘CLASSPATH’] := ClassPath; { Use the modified environment to launch Java. } EnvBlock := GetEnvBlock; try LaunchApp(JvmPath, JavaClass, Dir, EnvBlock); finally FreeEnvBlock(EnvBlock); end; end.

May 1997

17

WINDOWS 95 PROGRAMMING

Abbreviating ListBox entries

B

y default, ListBox components don’t display a horizontal scrollbar. Depending on the width of the ListBox and the exact length of a given item’s text, it may not be obvious that the text is too long to fit in the ListBox. In other cases, the ListBox clearly truncates the text, as shown in Figure A.

Figure A

If a given item’s text string is too long, a ListBox may truncate the entry.

In this article, we’ll show how you can create ListBox components that display an ellipsis (…) at the end of text strings that are too long to display correctly. To accomplish this task, we’ll take advantage of the new Windows 95 DrawTextEx() function and the DT_END_ELLIPSIS attribute.

Owner-drawn ListBox basics Delphi allows you to create three basic types of ListBox components: standard, owner-drawn variable-size, and owner-drawn fixed-size. If we’re going to modify the appearance of individual menu items based on their length, we obviously need to use one of the ownerdrawn variations. Furthermore, since we’re not going to draw anything other than fixed-height text, we can use the owner-drawn fixed-size style. To use this style, you simply set the ListBox component’s Style property to lbOwnerDrawFixed. However, this is just the beginning of the process. Now, Delphi will call the ListBox component’s OnDrawItem event handler each time it needs to draw or update one of the menu items. The format of an OnDrawItem event handler is procedure TForm1.ListBox1DrawItem(Control: TWinControl; Index: Integer; Rect: TRect; State: TOwnerDrawState);

The parameters of this method are crucial in displaying the text in the ListBox.

18

Delphi Developer’s Journal

2.0

The first parameter, Control, is a reference to the ListBox that needs its text drawn. The second parameter, Index, is the position of the item we need to draw. The third parameter, Rect, is the bounding rectangle of the item in the ListBox (all drawing should take place inside this rectangle). The fourth and final parameter, State, describes whether the ListBox currently has the focus, whether the item we’re drawing is selected, and so on. By default, Delphi does many things to help you draw owner-drawn ListBox items in a manner consistent with other Windows applications, such as providing the OnDrawItem and OnMeasureItem events. Some of the most important Delphi actions involve setting the Pen and Brush properties of the ListBox component’s Canvas property prior to calling your OnDrawItem event handler. In particular, each time that Delphi calls the OnDrawItem event handler, it will set the Pen and Brush properties to draw the item’s text and background in the appropriate colors. Because of this feature, you can frequently ignore the State parameter of the OnDrawItem event handler and let Delphi set up the drawing environment for you.

Drawing ellipses In your OnDrawItem event handler, you need to take two actions to correctly display items that are too long. First, you need to erase any previous information in the item’s bounding rectangle. The easy way to do this is to call the ListBox component’s Canvas.FillRect() method. As we mentioned above, Delphi sets the Pen and Brush properties prior to calling your OnDrawItem event handler, so you don’t need to modify them prior to calling FillRect(). The second step you need to take is to draw the text. Normally, you might use the DrawText() Windows function. However, Windows 95 provides a new function, DrawTextEx(), which gives you some additional options for displaying the text string. Specifically, you can call the DrawTextEx() function using the following format: DrawTextEx(ListBox1.Canvas.Handle, PChar(ListBox1.Items[Index]), Length(ListBox1.Items[Index]), Rect, DT_END_ELLIPSIS or DT_LEFT, nil);

This call passes the Handle property of the ListBox component’s Canvas, the text of the item we need to draw (as a null-terminated string), the actual length of the string, and the DT_END_ELLIPSIS and DT_LEFT attributes to the DrawTextEx function.

That’s it. This simple call will now draw each ListBox item normally, unless it’s too long to fit inside the available rectangle. If so, the call truncates the string and adds an ellipsis at the end.

Clipping ListBox tex… To see this technique in action, launch Delphi and create a blank form project. On the main form, place a ListBox component, and set its Style property to lbOwnerDrawFixed. Next, place an Edit component below the ListBox, a button below the Edit component, and a label below the button. (Adjust the size of the Edit component to match the width of the ListBox, and adjust the size of the label to be as wide as possible.) Double-click the button, and modify the OnClick event handler as follows (we’ve highlighted in red the code you’ll need to enter):

Delphi Developer’s Journal (ISSN #1082-3948) is published monthly by The Cobb Group. Prices

Domestic ................................. $69/yr ($7.50 each) Outside US ............................. $89/yr ($8.95 each)

Phone

Toll free (US) .......................... Toll free (UK) .......................... Local ....................................... Customer Relations fax ......... Editorial Department fax ........

Address

You may address tips, special requests, and other correspondence to The Editor, Delphi Developer’s Journal 9420 Bunsen Parkway, Suite 300 Louisville, KY 40220

procedure TForm1.Button1Click(Sender: TObject); begin ListBox1.Items.Add(Edit1.Text); end;

Or you can send Internet mail to [email protected] For subscriptions and fulfillment questions and requests for group subscriptions, address your letters to Customer Relations 9420 Bunsen Parkway, Suite 300 Louisville, KY 40220

This code copies text from the Edit component to new items in the ListBox. Double-click on the ListBox to create an OnClick event handler, and modify it to match the following method:

Or send Internet mail to [email protected] Copyright

procedure TForm1.ListBox1Click(Sender: TObject); begin Label1.Caption := ListBox1.Items[ListBox1.ItemIndex]; end;

This code copies the text for a ListBox item to the Label component when you click on the item. Next, create an OnDrawItem event handler for the ListBox, and modify it as shown below: procedure TForm1.ListBox1DrawItem(Control: TWinControl; Index: Integer; Rect: TRect; State: TOwnerDrawState); begin with (Control as TListBox) do begin Canvas.FillRect(Rect); DrawTextEx(Canvas.Handle, PChar(Items[Index]), Length(Items[Index]), Rect, DT_END_ELLIPSIS or DT_LEFT, nil); end; end;

(800) 223-8720 (0800) 961897 (502) 493-3300 (502) 491-8050 (502) 491-3433

Copyright © 1997, The Cobb Group. All rights reserved. Delphi Developer’s Journal is an independently produced publication of The Cobb Group. The Cobb Group reserves the right, with respect to submissions, to revise, republish, and authorize its readers to use the information submitted for both personal and commercial use. The Cobb Group and its logo are registered trademarks of Ziff-Davis Publishing Company. Microsoft Windows and MS-DOS are registered trademarks of Microsoft Corporation. Borland and Borland Delphi are registered trademarks of Borland International.

Staff

Editor-in-Chief ................... Tim Gooch Editor ................................. Joan McKim Publications Coordinator ... Maureen Spencer Product Group Manager .... Michael Stephens Circulation Manager ........... Mike Schroeder VP/Publisher ...................... Mark Crane President ........................... John A. Jenkins

Postmaster

Periodicals postage paid in Louisville, KY, and additional mailing offices. Postmaster: Send address changes to

Delphi Developer’s Journal P.O. Box 35160 Louisville, KY 40232

Back Issues

To order back issues, call Customer Relations at (800) 223-8720. Back issues cost $7.50 each; $8.95 outside the US. You can pay with MasterCard, VISA, Discover, or American Express, or we can bill you.

Now, build and run the application. When the main form appears, enter some random text in the Edit field, and click the button to add the text to the list box. As Figure B on the next page shows, if you enter text that’s longer than the width of the ListBox component, you’ll see an ellipsis appear at the end of that item.

May 1997

19

PERIODICALS MAIL

Borland Delphi Credit Card Advisor Line (800) 330-3372 Please include account number from label with any correspondence.

After you’ve entered a few items in the list box, click on one of the entries. When the text of that item appears in the label, confirm that the text is the actual item string and doesn’t contain the ellipsis we used to display the string, as shown in Figure C.

Figure B

If you’re drawing filenames… If you’re displaying filenames in a ListBox component, you’ll want to take a slightly different approach. When you display a filename, the end of the full path is as important (if not more so) than the beginning and middle. It would be nice if there were a simple way to display only the drive letter and first directory name, and then skip the middle portion of the pathname and display the actual filename. In fact, you can do this by using the attribute DT_PATH_ELLIPSIS with the DrawTextEx() function. You’ll begin by changing the DT_END_ELLIPSIS attribute in the OnDrawItem event handler to DT_PATH_ELLIPSIS and rebuilding the project. After the main form appears, enter a string in the Edit field that resembles a pathname, such as

You’ll see ellipses at the end of items too long for the list box.

Figure C

C:\Program Files\Delphi 2.0\readme.txt

When you add this string to the ListBox, you’ll see the beginning and end of the pathname, as shown in Figure D. Unfortunately, you can’t use the attribute DT_PATH_ELLIPSIS with normal list box entries. If you do, you’ll see something like ...This is a normal text string

Since we use the DrawTextEx() function only to display the ListBox items, the actual item text doesn’t change.

Figure D

If the end of the string is truncated, the list box won’t display an ellipsis at the end of the item.

Conclusion Owner-drawn ListBox components are powerful tools for displaying lists of text strings in a custom manner. By using the DT_END_ELLIPSIS attribute and the DrawTextEx() function, you can ensure that your users know whether they’re missing something when they view a list item. ❖ You can use the DT_PATH_ELLIPSIS attribute with the DrawTextEx() function to display pathnames correctly.

20

Delphi Developer’s Journal

Printed In the USA This journal is printed on recyclable paper.

2107