Edited By: John M. Miano miano@worldnet.att.net Version: 8 Last Updated: 06-Jun-1997
1.1. | What is the purpose of this document? |
2.1. | How can I locate problems that result when my component is used in the IDE? |
2.2. | How do I view assembly langugage the Delphi Generates? |
2.3. | I can create my control at run time but it crashes at design time. What is wrong? |
2.4. | How can I create a component that cannot be dropped on a form? |
2.5. | What is an easy way to backtrack through source code? |
2.6. | How can I add to the popup menu that is displayed when the right mouse button is clicked on my component? |
2.7. | How come I get I/O 103 in design mode? |
2.8. | Why are my property values not getting saved when I use a component editor? |
Section 3 - Using other components within a component
3.1. | How can I add a scroll bar component to my component and have it work at in design mode? |
3.2. | How do I create a Windows '95-style scroll bar? |
4.1. | Where is the documentation for the TDataLink class? |
4.2. | How can I tell the number of the record I am on in a data set? |
5.1. | How can I step through the VCL source while debugging? |
5.2. | My component references other components. How can I tell if a component my component references had been deleted? |
5.3. | What are component messages? |
5.4. | My control has focus but it is not getting keystroke messages. What's happening? |
Section 6 - Other Sources of Information
6.1. | Are there any books one how to write Delphi components? |
6.2. | Are there any good web sites with information on how to write components? |
Section 7 - Persistent Objects
7.1. | How can I save a complex object containing child objects to the .DFM file. |
7.2. | How can I tell if my constructor is being called for an object being loaded from a stream? |
7.3. | How can I tell if my component's properties are being saved correctly to the form file? |
8.1. | Is there YACC and LEX for Delphi? |
8.2. | How can I write code to display JPEG files? |
Section 9 - Basic Programming Techniques
9.1. | How do I create a dynamic array of objects? |
9.2. | Where is Wincrt in Delphi 2.0? |
9.3. | What should the base class be for my component? |
Section 10 - Advanced Programming Techniques
10.1. | Is there a Delphi equivalent to C++'s I/O stream classes? |
10.2. | How can I get the text equivalent for an enumerated type? |
Section 11 - Component Virtual Methods
11.1. | How can I find out when my component has had its window handle created? |
11.2. | How can I tell when all the components on my form have been loaded? |
11.3. | Where is the best place to draw my control? |
11.4. | How do I change the Window Style for my control? |
Section 12 Windows API Functions
12.1. | I am trying to scroll the contents of my control but I get an ugly flicker effect. How can I eliminate this? |
12.2. | How can I restart windows? |
12.3. | How can I batch updates to when changing the appearance of my control? |
13.1. | How come my control does not have a 3D border even when I have CTL3D set to True? |
13.2 | How do I implement a BorderStyle property? |
14.1. | How do I stop my control from flickering when it gets repainted? |
15.1. | How come my control does not get keystroke messages for the arrow keys? |
15.2. | Is there an equivalent of Visual Basic's "DoEvents" statement? |
The purpose of this document is to answer common or undocumented questions related to writing Delphi components. After spending a ridiculous amount of time trying to decipher the TDataLink component it occured to me that there ought to be some way of recording and sharing solutions to the problems encountered while writing controls. All information is provided as is. There are no guarantee as to its correctness.
If you have any questions you would like to be answered or have any contributions you think would be suitable for inclusion please send them to the editor. Reports of errors or omissions are also welcome.
In addition to including more answers and questions I am trying to expand in two more areas:
The only solution to locating problems I have found is to:
From Glen Boyd
Run the REGEDIT program and go to "HKEY_CURRENT_USER\Software\Borland\Delphi\2.0\Debugging" and add a string value called EnableCPU and set its string value to 1. This adds the CPU window to the view menu. The CPU window is active at run time for stepping through and stuff like that.
Constructor Create (AOwner : TComponent) ; Override ; Destructor Destroy ; Override ;
Type TComplex = Record RealPart : Double ; ComplexPart : Double ; End ; class TMyComponent = Class (TComponent) Private F1 : TComplex ; Published Property P1 : TComplex Read F1 Write F1 ; End ;
From Ray Lischner
If you don't want the user to be able to drop the component on a form then use then RegisterNoIcon and RegisterClass procedures.
From Ray Konopka
When viewing the source for the VCL units, it's definitely a good idea to become comfortable with bookmarks in the editor. That is, Ctrl+Shift+N, when N is a number 0-9, to set a bookmark. Jump to a bookmark using Ctrl+N.
You do this by creating a Component Editor. One would think that a component editor would be something that is called from the popup menu but it actually controls what appears in the menu or rather the items that your component can add to the menu.
The steps to follow are:
You probably have WriteLn statements in your code.
I have found that when using a component editor that a component's properties will not be saved. The property appears to be correct in design mode but when you run or save the property is set to a different value.
It appears that a property editor needs to call
Designer.Modifiedto let Delphi know that you have changed a property value.
You need to define your own scroll bar class that intercepts the CM_DESIGNHITTEST message.
TMyScrollBar = class (TScrollBar) Procedure CMDesignHitTest (var Message : TCMDesignHitTest) ; Message CM_DESIGNHITTEST ; End ; Procedure TMyScrollBar.CMDesignHitTest (var Message : TCMDesignHitTest) ; Begin Message.Result := 1 ; End ;When your component creates one of these scroll bars it needs to use
TMyScrollBar.Create (Nil)rather than
TMyScrollBar.Create (Self)otherwise the scroll bar will display sizing handles when it is click. This means you need to be sure to explicitly free the scroll bar in your component's destructor.
You need to set the page size for the scroll bar. The following code sequence illustrates this:
Procedure SetPageSize (ScrollBar : TScrollBar ; PageSize : Integer) ; Var ScrollInfo : TScrollInfo ; Begin ScrollInfo.cbSize := Sizeof (ScrollInfo) ; ScrollInfo.fMask := SIF_PAGE ; ScrollInfo.nPage := PageSize ; SetScrollInfo (ScrollBar.Handle, SB_CTL, ScrollInfo, True) ; End ;To retrieve the page size use:
Function GetpageSize (ScrollBar : TScrollBar) ; Var ScrollInfo : TScrollInfo ; Begin If HandleAllocated Then Begin ScrollInfo.cbSize := Sizeof (ScrollInfo) ; ScrollInfo.fMask := SIF_PAGE ; GetScrollInfo (ScrollBar.Handle, SB_CTL, ScrollInfo) ; Result := ScrollInfo.nPage ; End ; End ;
The C++Builder documentation contains a description of the TDataLink class. It is not in any Delphi documentation so far. For those of you who do not have C++Builder here is the description that has been in this document for a long time:
Properties:
Return Value:
True => Success False => Failure
The mechanism for having the TDataLink object communicate with a component is to override these procedures.
RecordChanged (Nil)
Use the ActiveRecord to determine which record within the buffer window is the current one.
It is not possible to force a scroll of the buffer window.
At first glance the RecNo property for a data set appears to be what you want but unfortunately this only works with dBase and Paradox tables.
The way you can keep track of the current record number is to create a class that derives from TDataLink. The main things you need to do are:
Copy the VCL source modules you are interested in stepping through to your project directory then rebuild the VCL library. You will then be able to step through the VCL source modules.
From Max Nilson
A little documented part of TComponent and its decendants is the the Notification method. This method is primarily used to detect cases where components that you are referencing are being deleted. It does have other features but I have not yet thought of a reason to use them. For Borlands explanation of these things search for Notification and FreeNotification in the VCL help. Don't bother looking in the Component Writer's Guide 8-)
When ever you reference another component from your component, for example including a TDataSource property in your component, you should override the Noficication method and respond to it by checking that the component you are referencing is not being deleted. You should also use the FreeNotification method to ensure that you are notified even it the TComponent you are referencing is in another module. By default you only recieve notifications of componets in the same module as your component has been placed, and now that Borland has provided data modules you are sure to run this case more often than you expect.
If you don't use the Nofification method you will find (as I did) that deleting a component that you reference will place the Delphi IDE into an extremely unstable state. It doesn't quite crash, but its very hard to do anything afterwards.
Here is an example showing just the critical methods with a single reference to another component:
type TMyComponent = class(TComponent) private FDataSource: TDataSource; procedure SetDataSource(Value: TDataSource); protected procedure Notification(AComponent: TComponent; Operation: TOperation); override; published property DataSource: TDataSource read FDataSource write SetDataSource; end; procedure TMyComponent.SetDataSource(Value: TDataSource); begin if Value <> FDataSource then begin FDataSource := Value; if FDataSource <> nil then // Tell the component that we are interested in its fate FDataSource.FreeNotification(Self) end end; procedure TMyComponent.Notification (AComponent : TComponent; Operation : TOperation); begin inherited Notification(AComponent, Operation); // If this is the component we are referencing then remove our reference if (Operation = opRemove) and (AComponent = FDataSource) then FDataSource := nil end;
Component messages are used much like regular Windows messages except that they are used for notification of events that are only applicable to Delphi components. If you have a component that publishes the Font property the component probably needs to be repainted if any subproperties in the Font are changed. Changing the Font property does not necessarily generate a Windows event but the control still needs to know about the change. Component messages serve this purpose.
It appears that virtual methods could have been used in place of component messages. Presumably messages are used in order to keep the size of the virtual dispatch table from getting out of hand.
The books "Secrets of Delphi 2.0" has descriptions for the individual component messages.
This is a listing of some of the component messages andwhat they do. The messages marked "Notification Only" do not pass any useful information to the message handler and do not expect the message handler to return a value.
The book that has become the standard for writing components is:
"Developing Delphi Components" by Ray Konopka, Coriolis Group BooksWhile this book not specifically on how to write components it has a lot of information that is invaluable to the component writer:
"Secrets of Delphi 2" by Ray Lischner, Waite Group Press
Another book on writing components that has information not found in Konopka's book is
"Programming Delphi Custom Components" by Fred Bulback, M&T Books
The largest Delphi web site is "The Delphi Super Site" at
http://sunsite.icm.edu.pl/~robert/delphiThis page has links to many other Delphi sites.
I have found Component source code on the following sites as well:
http://www.coast.net/~jkellerYou can also find Delphi sites by using:
http://www.pobox.com/~bstowers/delphi
Yahoo: www.yahoo.comUnfortunately Web sites have a nasty habit of disappearing or moving. Please notify the maintainer these addresses are out of date.
Alta Vista: www.altavista.digital.com
I have tried all sorts of schemes using DefineProperties and WriteComponents and they all failed to work. As far as I can tell the only way to do this is to use Delphi's default mechanism to store your child objects.
A sequence that does work for saving to a stream is:
Procedure TMyComponent.GetChildren (Proc : TGetChildProc) ; Begin Proc (Child1) ; Proc (Child2) ; ... Proc (Childn) ; End ;Getting the objects out of the stream is a little trickier. Your parent object may need to overload the GetChildOwner and GetChildParent functions. Otherwise Delphi will try to make the child owned by the form. (In Delphi V1 you need to override the Readstate method.)
csLoading is not set in ComponentState until immediately after the component is created. However the component's owner will already have this set so try
Constructor TMyClass.Create (AOwner : TComponent) ; Begin If csLoading in AOwner.ComponentState Then Begin End Else Begin End ; End ;
There are a couple of easy ways to view properties as they are stored in the form file:
There is a YACC and LEX written by Albert Graef (ag@muwiinfa.geschichte.uni-mainz.de) for Turbo Pascal that works with Delphi. This can be found on many shareware sites (search for Pascal and YACC).
One location is /msdos/turbopas/tply30a1.zip and /msdos/turbopas/tply30a2.zip in the Simtel archives.
nomssi@physik.tu-chemnitz.de has a Pascal library based of the Independent JPEG Group's free JPEG library. It is available from the following locations:
ftp://druckfix.physik.tu-chemnitz.de/pub/nv/
http://www.tu-chemnitz.de/~nomssi/pub/pasjpeg.zip
The easiest way to create an array of objects is to use a TList control. I often find it helpful to create specialized classes that derive from TList. The following class shows how to create a list for a specific type of object that has a little better error reporting than the basic TList. For you own list you may need to implement more methods.
TListOfMyObject = class (TList) Private Function GetItems (Index : Ordinal) : TmyObject ; public Property Items [Index : Ordinal] : TmyObject Read GetItems ; Procedure Add (AObject : TmyObject) ; End ; Function TListOfMyObject.GetItems (Index : Ordinal) : TmyObject ; Begin If Index >= Count Then raise Exception.CreateFmt ('Index (%d) outside range 1..%d', [Index, Count -1 ]) ; Result := Inherited Items [Index] ; End ; Procedure TListOfMyObject.Add (AObject : TmyObject) ; Begin Inherited Add (AObject) ; End ;
There is no WinCrt unit in Delphi 2.0. It has been replaced by a project options setting. On the linker page there is an option to create a console application. Check this rather than use the WinCrt unit.
If you are deriving from an existing component then the class of the component you wish to derive from should be the base class. Several of the registered VCL component classes are immediate ancestors of corresponding custom classes (e.g. TMemo descends from TCustomMemo). These custom classes implement most of the functionality of the control but they do not publish many of the properties that they define. In most cases you are probably better off deriving your component from the custom class rather than the well known registered class.
If you are creating a component from scratch then TCustomControl is most likely your best choice. This is used for visual, windowed controls.
Other less likely alternatives are:
TGraphicControl - For visual controls that have no associated window. These cannot receive input focus.
TComponent - Non-visual component.
TWinControl - To create a control that derives from an existing control that was not created specifically for Delphi.
Yes and no. Delphi allows you to create your own "Text-File Device Driver" which allows you to use standard Delphi I/O procedures for non-standard I/O streams such as Unix <LF> files or network connections. These are not as powerful as C++ I/O Streams but it is possible to get around their limitations.
The procedure for creating a Text-File Device Driver is reasonably well documented in the "Object Pascal Langue Guide" and there is one example in the VCL source in PRINTER.PAS.
Delphi also has stream classes for writing objects to a stream. These are not as "general purpose" as C++'s I/O streams.
Use the function GetEnumName located in the module TypInfo.
Type TMyType = (Value1, Value2) ; ... Var TypeValue : TmyType ; ... WriteLn (GetEnumName (TypeInfo (TMyType), Ord (TypeValue)) ;The module TypInfo has many other functions for obtaining information about types.
The book "Secrets of Delphi 2.0" has a lot of information on how to us the TypInfo module.
A control's window handle is created by the CreateWnd method. If you have processing that needs to be performed after the window handle is created then you can override CreateWnd and do
Procedure TMyClass.CreateWnd ; Begin Inherited CreateWnd ; { Don't forget or you'll never get a window handle. } { Your processing goes here. } End ;
The Loaded method is called for each component on a form after all controls on the form have been loaded from the stream.
Procedure TMyClass.Loaded ; Begin Inherited Loaded ; { Clears the csLoading in ComponentState } { Your processing goes here. } End ;
You could intercept the WM_PAINT message and set up your control's canvas however will VCL do all this work for you if you simply override the Paint method in your control.
Procedure TMyClass.Paint ; Begin { You only need to call inherited Paint if your class inherits directly from TCustomControl or TGraphicControl. You may need to call this if your control inherits from an existing control. } Inherited Paint ; { Your processing goes here. } End ;
The CreateParams method is used to set up the window style and all the other arguments that are passed to CreateWindowEx to create the control's window.To change the window style use something like this which creates a window with or without a vertical scroll bar.
procedure TMyControl.CreateParams(var Params: TCreateParams) ; Begin Inherited CreateParams (Params) ; IF IWantAScrollBar Then Params.Style := Params.Style OR WS_VSCROLL Else Params.Style := Params.Style AND NOT WS_VSCROLL ; End ;
The easiest way to scroll the elements of a control is to change all of their coordinates then to force a repaint of the control. Unfortunately this produces the flicker effect.
The best way to reduct this flickering is to use the ScrollWindow or ScrollWindowEx Windows API function.
Another source of flickering can be from Windows using two messages to paint: WM_PAINT and WM_ERASEBKGND. You may want to intercept all of the WM_ERASEBKGND messages and do all of your painting, including the background, in response to WM_PAINT messages in the Paint method.
Use the ExitWindowsEx function.
Sending the WM_SETREDRAW message to your control can either set or clear the flag used to determine if a control is redrawn.
CTL3D has no effect unless csFramed is set in ControlStyle. Try something like this inside your constructor:
ControlStyle := ControlStyle + [csFramed] ;
The trick to having a control border is that the border must be created when the control's Window handle is created.
FBorderStyle : TBorderStyle ; Procedure SetBorderStyle (Style : TBorderStyle) ; Property BorderStyle : TBorderStyle Read FBorderStyle write SetBorderStyle ; procedure CreateParams(var Params: TCreateParams) ; Override ; procedure TMyControl.CreateParams(var Params: TCreateParams) ; Begin Inherited CreateParams (Params) ; If FBorderStyle = bsSingle Then Params.Style := Params.Style Or WS_BORDER Else Params.Style := Params.Style And Not WS_BORDER ; End ; Procedure TMyControl.SetBorderStyle (Style : TBorderStyle) ; Begin IF Style <> FBorderStyle Then Begin FBorderStyle := Style ; { Create new window handle for the control. } RecreateWnd ; End ; End ;
If you do not include csOpaque in ControlStyle then Invalidate calls will cause the control's background to be erased. If you draw your control's background in the Paint method then you should do this in your constructor:
ControlStyle := ControlStyle + [csOpaque] ;From Max Nilson
Another possible cause of flickering can be found by examining the TWinControls WM_ERASEBKGND handling. Whenever this message is received by a windowed control the VCL erases the controls background to its default color. If your controls is a TWinControl decendant, and draws itself in something other than the default background color (e.g. a bitmap), then this can cause some flickering as the background is cleared and then you repaint it again.
By using the following method in your component you inform Windows that you will handle _all_ of the necessary drawing yourself. You just have to ensure that you do paint _all_ of your controls surface, because anything you don't repaint will contain random garbage pixels. This will also speed your control (very slightly 8-) by saving one rectangle fill operation.
type TMyComponent = class(TWinControl) ... protected procedure WMEraseBkgnd(var Message: TWMEraseBkgnd); message WM_ERASEBKGND; ... end; procedure TBMyComponent.WMEraseBkgnd(var Message: TWMEraseBkgnd); begin Message.Result := 0 end;
To receive keystroke messages for arrow keys you have to handle the WM_GETDLGCODE messages. In the messages handler return DLGC_WANTARROWS. If you do not handle WM_GETDLGCODE Windows intercepts the arrow keys and uses them to move among controls.
From Max Nilson
To receive keystroke messages for arrow keys you have to handle the CM_WANTSPECIALKEY messages. The CM_WANTSPECIALKEY allows a much more 'fine grained' way of deciding if you require a special key than responding to the WM_GETDLGCODE messages. The control message CM_WANTSPECIALKEY is sent to a control whenever a special key is being handled.
These special keys are VK_TAB, VK_LEFT, VK_RIGHT, VK_UP, VK_DOWN, VK_RETURN, VK_EXECUTE, VK_ESCAPE and VK_CANCEL. If the message result is non zero then the key is passed onto the KeyPress method for you to handle, otherwise it is passed onto the controls parent form for Delphi's standard navigation handling. The Delphi standard navigation is where Delphi handles the Tab, Shift-Tab and arrow key motion between controls, without needing to use the default Windows dialog manager at all!
A simple example:
type TMyComponent = class(TWinControl) ... protected procedure CMWantSpecialKey(var Message: TCMWantSpecialKey); message CM_WANTSPECIALKEY; ... end; procedure TMyComponent.CMWantSpecialKey (var Message: TCMWantSpecialKey); begin inherited; // We want to handle the left arrow ourselves if Message.CharCode = VK_LEFT then Message.Result := 1; end;The 'fine grained' I refered to above comes from this ability to examine a specific keypress and decide if you need to handle the key your self, or allow it to continue on into Delphi's handler. If you had a single control with three images, for example, you could allow the left and right arrows to more back and forth between them, and also let the user to move onto the next tab stop from the last image on your control, by allowing Delphi to handle the keypress instead of your internal control handling.
Yes. Application.ProcessMessages
Copyright 1996 - John Miano Contributors Glen Boyd Stefan Hoffmeister Ray Konopka Ray Lischner Max Nilson