The real fun (if we can say so) is when you spend time to do useless things, otherwise writing programs can be considered as a work. Although there is some effort involved, you can really have a lot of fun in Delphi.
This paper describes a number of ways to loose time and have fun in Delphi, writing components, stretching Delphi and Windows to the limit, and configuring the environment with Experts and other tools. Being a "fun" presentation, some multimedia will be involved, of course.
We want to built a component, but how do we build one? Please refer to a specific session, article, or book, to discover everything about writing components. For this presentation you only need to know that a component is a subclass of class TComponent (or one of its subclasses), that there are three kinds of components (non-visual components, window-based components, and graphical components), and that components have methods, properties, and events.
Instead of discussing components in general, I prefer showing you how to build some useless ones (in this section) and some very strange ones (in the next section). For the moment, let me focus on how you can make a lot of work to obtain very little, but still have some fun in the process (and in the result).
Still, we have to write some code. In fact if we want our component to have standard properties and events we have to list them:
type TNothing = class(TGraphicControl) public constructor Create (Owner: TComponent); override; published property Width default 50; property Height default 50; property Align; property ShowHint; property Visible; ... end;We also need to write the code of the Create constructor of the component (which sets the default values) and the Register procedure:
constructor TNothing.Create (Owner: TComponent); begin // call parent class constructor first inherited Create (Owner); // set the size Width := 50; Height := 50; end; procedure Register; begin RegisterComponents('DDHB', [TNothing]); end;
I've actually written two versions of theis component. The simplest version redefines a Windows message, with the following code, in which the mouse move message handler looks for and eventually calls the OnClick event handler:
type TAutoButton1 = class(TButton) private procedure WmMouseMove (var Msg: TMessage); message wm_MouseMove; end; procedure TAutoButton1.WmMouseMove (var Msg: TMessage); begin inherited; if Assigned (OnClick) then OnClick (self); end;The second version has much more code, since I try to repeat the mouse OnClick event when the user moves the mouse over the button or after a given amount of time. Here is the declaration of the class:
type TAutoKind = (akTime, akMovement, akBoth); TAutoButton2 = class(TButton) private FAutoKind: TAutoKind; FMovements: Integer; FSeconds: Integer; // really private CurrMov: Integer; Capture: Boolean; MyTimer: TTimer; procedure EndCapture; // message handlers procedure WmMouseMove (var Msg: TWMMouse); message wm_MouseMove; procedure TimerProc (Sender: TObject); procedure WmLBUttonDown (var Msg: TMessage); message wm_LBUttonDown; procedure WmLButtonUp (var Msg: TMessage); message wm_LButtonUp; public constructor Create (AOwner: TComponent); override; published property AutoKind: TAutoKind read FAutoKind write FAutoKind default akTime; property Movements: Integer read FMovements write FMovements default 5; property Seconds: Integer read FSeconds write FSeconds default 10; end;The code is quite complex, and we don't have time to cover the details. Basically when a user moves the mouse over the area of the button (WmMouseMove) the component starts a timer or counts the move messages. After a given amount of time, or when the proper number of move messages has been reached, the component simulates the mouse click event. The plain OnClick events do not work properly, but I decided I don't care...
procedure TAutoButton2.WmMouseMove (var Msg: TWMMouse); begin inherited; if not Capture then begin SetCapture (Handle); Capture := True; CurrMov := 0; if FAutoKind <> akMovement then begin MyTimer := TTimer.Create (Parent); if FSeconds <> 0 then MyTimer.Interval := 3000 else MyTimer.Interval := FSeconds * 1000; MyTimer.OnTimer := TimerProc; MyTimer.Enabled := True; end; end else // capture begin if (Msg.XPos > 0) and (Msg.XPos < Width) and (Msg.YPos > 0) and (Msg.YPos < Height) then begin // if we have to consider movement... if FAutoKind <> akTime then begin Inc (CurrMov); if CurrMov >= FMovements then begin if Assigned (OnClick) then OnClick (self); EndCapture; end; end; end else // out of the area... stop! EndCapture; end; end; procedure TAutoButton2.EndCapture; begin Capture := False; ReleaseCapture; if Assigned (MyTimer) then begin MyTimer.Enabled := False; MyTimer.Free; MyTimer := nil; end; end; procedure TAutoButton2.TimerProc (Sender: TObject); begin if Assigned (OnClick) then OnClick (self); EndCapture; end; procedure TAutoButton2.WmLBUttonDown (var Msg: TMessage); begin if not Capture then inherited; end; procedure TAutoButton2.WmLButtonUp (var Msg: TMessage); begin if not Capture then inherited; end;
If you really want to get rid of edit boxes, here comes the solution: a label input components, a label component that can get the user input. This is an overly complex component, because labels have no way to get the input from the keyboard. They are graphical components, not based on a window, so they cannot receive the input focus, and they cannot get text. For this reason I've developed this example in two steps.
First step is an input-button component (quite simple) to show you the input code:
type TInputButton = class(TButton) private procedure WmChar (var Msg: TWMChar); message wm_Char; end; procedure TInputButton.WmChar (var Msg: TWMChar); var Temp: String; begin if Char (Msg.CharCode) = #8 then begin Temp := Caption; Delete (Temp, Length (Temp), 1); Caption := Temp; end else Caption := Caption + Char (Msg.CharCode); end;The input label, instead, has to do a number of tricks to bypass the problems related to its internal structure. Basically the problem can be solved by creating other hidden components (why not an edit box?) at runtime. Here is the declaration of the class:
type TInputLabel = class (TLabel) private MyEdit: TEdit; procedure WMLButtonDown (var Msg: TMessage); message wm_LButtonDown; protected procedure EditChange (Sender: TObject); procedure EditExit (Sender: TObject); public constructor Create (AOwner: TComponent); override; end;When the label is created it generates the edit box, and set some event handler for it. In fact as the user clicks on the label the focus is moved to the (invisible) edit box, and we use its events to update the label. Notice in particular the code used to mimic the focus for the label, which is based on the DrawFocusRect API call:
constructor TInputLabel.Create (AOwner: TComponent); begin inherited Create (AOwner); MyEdit := TEdit.Create (AOwner); MyEdit.Parent := AOwner as TForm; MyEdit.Width := 0; MyEdit.Height := 0; MyEdit.TabStop := False; MyEdit.OnChange := EditChange; MyEdit.OnExit := EditExit; end; procedure TInputLabel.WMLButtonDown (var Msg: TMessage); begin MyEdit.SetFocus; MyEdit.Text := Caption; (Owner as TForm).Canvas.DrawFocusRect (BoundsRect); end; procedure TInputLabel.EditChange (Sender: TObject); begin Caption := MyEdit.Text; Invalidate; Update; (Owner as TForm).Canvas.DrawFocusRect (BoundsRect); end; procedure TInputLabel.EditExit (Sender: TObject); begin (Owner as TForm).Invalidate; end;
The sound button component has two brand new properties:
type TDdhSoundButton = class(TButton) private FSoundUp, FSoundDown: string; protected procedure MouseDown(Button: TMouseButton; Shift: TShiftState; X, Y: Integer); override; procedure MouseUp(Button: TMouseButton; Shift: TShiftState; X, Y: Integer); override; published property SoundUp: string read FSoundUp write FSoundUp; property SoundDown: string read FSoundDown write FSoundDown; end;These sounds are played when a button is pressed or realeased:
procedure TDdhSoundButton.MouseDown( Button: TMouseButton; Shift: TShiftState; X, Y: Integer); begin inherited; PlaySound (PChar (FSoundDown), 0, snd_Async); end; procedure TDdhSoundButton.MouseUp(Button: TMouseButton; Shift: TShiftState; X, Y: Integer); begin inherited; PlaySound (PChar (FSoundUp), 0, snd_Async); end;
To provide the images to the animated button, I've decide to rely on the ImageList component, which allows you to place many bitmaps in a single container. Each of the bitmaps will be displayed after the previous one, providing animated effects. The code is quite long, and is available for reference in the companion source code, but it is not in the paper.
This is the class definition:
type TAutoFont = class(TComponent) private FTimer: TTimer; FInterval: Cardinal; FFixedSize, FAllAlike: Boolean; protected procedure OnTimer (Sender: TObject); procedure SetInterval (Value: Cardinal); public constructor Create (AOwner: TComponent); override; published property Interval: Cardinal read FInterval write SetInterval default 10000; property FixedSize: Boolean read FFixedSize write FFixedSize default True; property AllAlike: Boolean read FAllAlike write FAllAlike default True; end;The only relevant method of the class is the OnTimer event handler, which includes the font changing code:
procedure TAutoFont.OnTimer (Sender: TObject); var I: Integer; Fnt: TFont; begin (Owner as TForm).Font.Name := Screen.Fonts [Random (Screen.Fonts.Count)]; if not FFixedSize then (Owner as TForm).Font.Size := Random (36); if not FAllAlike then begin Fnt := TFont.Create; Fnt.Assign ((Owner as TForm).Font); for I := 0 to Owner.ComponentCount - 1 do begin Fnt.Name := Screen.Fonts [Random (Screen.Fonts.Count)]; if Owner.Components [I] is TWinControl then SendMessage ( TWinControl (Owner.Components [I]).Handle, wm_SetFont, Fnt.Handle, MakeLong (1,0)); end; Fnt.Free; end; end;
type TSmartClose = class(TComponent) public procedure Close; end; procedure TSmartClose.Close; begin (Owner as TForm).AutoScroll := False; repeat (Owner as TForm).ScaleBy (93, 100); Application.ProcessMessages; until (Owner As TForm).Height < 50; (Owner as TForm).Close; end;
Again the most relevant portion of the code is in the OnTimer event handler:
type TScreenVirus = class(TComponent) private FTimer: TTimer; FInterval: Cardinal; FColor: TColor; FRadius: Integer; protected procedure OnTimer (Sender: TObject); procedure SetInterval (Value: Cardinal); public constructor Create (AOwner: TComponent); override; procedure StartInfection; published property Interval: Cardinal read FInterval write SetInterval; property Color: TColor read FColor write FColor default clRed; property Radius: Integer read FRadius write FRadius default 10; end; constructor TScreenVirus.Create (AOwner: TComponent); begin inherited Create (AOwner); FTimer := TTimer.Create (Owner); FInterval := FTimer.Interval; FTimer.Enabled := False; FTimer.OnTimer := OnTimer; FColor := clRed; FRadius := 10; end; procedure TScreenVirus.StartInfection; begin if Assigned (FTimer) then FTimer.Enabled := True; end; procedure TScreenVirus.SetInterval (Value: Cardinal); begin if Value <> FInterval then begin FInterval := Value; FTimer.Interval := Interval; end; end; procedure TScreenVirus.OnTimer (Sender: TObject); var hdcDesk: THandle; Brush: TBrush; X, Y: Integer; begin hdcDesk := GetWindowDC (GetDesktopWindow); Brush := TBrush.Create; Brush.Color := FColor; SelectObject (hdcDesk, Brush.Handle); X := Random (Screen.Width); Y := Random (Screen.Height); Ellipse (hdcDesk, X - FRadius, Y - FRadius, X + FRadius, Y + FRadius); ReleaseDC (hdcDesk, GetDesktopWindow); Brush.Free; end;
type TFunCopyright = class(TComponent) private FCopyright, FAuthor: string; FDummy1, FDummy2: string; FLabel: TLabel; protected procedure SetLabel (Value: TLabel); public constructor Create (AOwner: TComponent); override; published property Copyright: string read FCopyright write FDummy1; property Author: string read FAuthor write FDummy2; property OutputLabel: TLabel read FLabel write SetLabel; end; constructor TFunCopyright.Create (AOwner: TComponent); begin inherited Create (AOwner); FAuthor := 'Marco Cant�'; FCopyright := '(c)MC 1997'; if csDesigning in ComponentState then begin with Owner as TForm do Caption := Caption + ' using a component by ' + FAuthor; with Application do Title := Title + ' using a component by ' + FAuthor; ShowMessage ('This form is using a component by ' + FAuthor); end else ShowMessage ('This program uses a component by ' + FAuthor); end; procedure TFunCopyright.SetLabel (Value: TLabel); begin if Value <> FLabel then begin FLabel := Value; FLabel.Caption := FCopyright; end; end;
type TSpecialIntProperty = class (TIntegerProperty) public function GetAttributes: TPropertyAttributes; override; procedure Edit; override; end;The important method is Edit, which is often used to show a dialog box (built in Delphi, as usual):
function TSpecialIntProperty.GetAttributes: TPropertyAttributes; begin Result := [paDialog, paReadOnly]; end; procedure TSpecialIntProperty.Edit; var PEForm: TSpinForm; begin PEForm := TSpinForm.Create (Application); try PEForm.Edit1.Text := GetValue; if PEForm.ShowModal = mrOK then SetValue (PEForm.Edit1.Text); finally PEForm.Free; end; end;In this code GetValue and SetValue are two special methods of the parent property editor, accessing to the data of the given property of the current component. To make this work you have to write also a proper registration procedure:
procedure Register; begin RegisterPropertyEditor (TypeInfo(Integer), TButton, '', TSpecialIntProperty); end;
RegisterPropertyEditor (TypeInfo(string), TSoundButton, 'SoundUp', TSoundProperty);
type TMyColorProperty = class (TColorProperty) public procedure Edit; override; end; procedure Register; implementation var nEditor: Integer; procedure TMyColorProperty.Edit; begin try case nEditor of 0: begin FormColor1 := TFormColor1.Create (Application); ... 1: begin FormColor2 := TFormColor2.Create (Application); ... 2: inherited Edit; end; finally nEditor := (nEditor + 1) mod 3; end; end; procedure Register; begin RegisterPropertyEditor (TypeInfo(TColor), TComponent, '', TMyColorProperty); end; initialization nEditor := 0; end.
This is actually an excuse to see how an expert is built. First derive a new class, with a bunch of overridden methods (required since they are virtual abstract):
type TBlankExpert = class (TIExpert) public function GetStyle: TExpertStyle; override; function GetName: string; override; function GetComment: string; override; function GetGlyph: HBITMAP; override; function GetState: TExpertState; override; function GetIDString: string; override; function GetMenuText: string; override; procedure Execute; override; end;Most of the methods have empty or default code. The only real code is in the Execute method:
function TBlankExpert.GetStyle: TExpertStyle; begin Result := esStandard; end; function TBlankExpert.GetName: String; begin Result := 'Blank Expert' end; function TBlankExpert.GetComment: String; begin Result := ''; // no thanks end; function TBlankExpert.GetGlyph: HBITMAP; begin Result := 0; // no thanks end; function TBlankExpert.GetState: TExpertState; begin Result := [esEnabled]; end; function TBlankExpert.GetIDString: String; begin Result := 'MarcoCantu.BlankExpert' end; function TBlankExpert.GetMenuText: String; begin Result := '&Blank Expert...' end; procedure TBlankExpert.Execute; var DirName: string; begin if MessageDlg ('Are you sure you want to exit'#13 + 'from the current project, saving it?', mtConfirmation, [mbYes, mbNo], 0) = mrYes then begin ToolServices.SaveProject; ToolServices.CloseProject; SelectDirectory (DirName, [sdAllowCreate, sdPerformCreate, sdPrompt], 0); ToolServices.OpenProject (DirName + '\Project1.dpr'); end; end;
The code of this example is fairly simple: Just write several for loops in which you allocate resources forever. Here are two methods:
procedure TForm1.ButtonWindowsClick(Sender: TObject); var NewForm: TForm; Hwnd: THandle; I: Integer; begin NewForm := TForm.Create (Application); NewForm.Show; NewForm.Update; // create a number of windows... try for I := 1 to 1000000 do begin Hwnd := CreateWindow ('button', 'Button', ws_child or ws_border or bs_pushbutton, I mod (ClientWidth - 40), I mod (ClientHeight - 20), 40, 20, Handle, 0, HInstance, nil); if Hwnd = 0 then raise Exception.Create ('Out of handles'); if (I mod 20) = 0 then NewForm.Caption := 'Created: ' + IntToStr (I); Application.ProcessMessages; end; finally ButtonWindows.Caption := Format ('Created: %d', [I]); NewForm.Free; end; end; procedure TForm1.ButtonPensClick(Sender: TObject); var H: THandle; I: Integer; begin try for I := 1 to 1000000 do begin H := CreatePen (ps_solid, 1, RGB (0, 0, 0)); if H = 0 then raise Exception.Create ('Out of handles'); if (I mod 20) = 0 then ButtonPens.Caption := Format ('Created: %d', [I]); Application.ProcessMessages; end; finally ButtonPens.Caption := Format ('Created: %d', [I]); end; end;
This last trick is explored by the UAE example. You can show a simple UAE message box, build a full fledged dialog box, with the details sub window, and even make a close button which doesn't want to be pressed.
The fake error form has a details button that shows open the second part of the form. This is accomplished by adding components out of the surface of the form itself, as you can see in its textual description:
object Form2: TForm2 AutoScroll = False Caption = 'Error' ClientHeight = 93 ClientWidth = 320 OnShow = FormShow object Label1: TLabel Left = 56 Top = 16 Width = 172 Height = 65 AutoSize = False Caption = 'The program has performed an illegal ' + 'operation. If the problem' + 'persist contact the software vendor.' WordWrap = True end object Image1: TImage Left = 8 Top = 16 Width = 41 Height = 41 Picture.Data = {...} end object Button1: TButton Left = 240 Top = 16 Width = 75 Height = 25 Caption = 'Close' TabOrder = 0 OnClick = Button1Click end object Button2: TButton Left = 240 Top = 56 Width = 75 Height = 25 Caption = 'Details >>' TabOrder = 1 OnClick = Button2Click end object Memo1: TMemo // out of the form! Left = 24 Top = 104 Width = 265 Height = 89 Color = clBtnFace Lines.Strings = ( 'AX:BX 73A5:495B' 'SX:PK 676F:FFFF' 'OH:OH 7645:2347' 'Crash 3485:9874' '' 'What'#39's going on here?') TabOrder = 2 end endWhen a user presses the details button the program simply update the size of the form:
procedure TForm2.Button2Click(Sender: TObject); begin Height := 231; end;A second form, which inherits from the first one, has an extra trick, a moving close button:
procedure TForm3.Button1Click(Sender: TObject); begin Button1.Left := Random (ClientWidth - Button1.Width); Button1.Top := Random (ClientHeight - Button1.Height); end;Finally, you can create a hole in a window by using the SetWindowRgn Win32 API function. This can really make users scream:
procedure TForm1.Button4Click(Sender: TObject); var HRegion1, Hreg2, Hreg3: THandle; Col: TColor; begin ShowMessage ('Ready for a real crash?'); Col := Color; Color := clRed; PlaySound ('boom.wav', 0, snd_sync); HRegion1 := CreatePolygonRgn (Pts, sizeof (Pts) div 8, alternate); SetWindowRgn ( Handle, HRegion1, True); ShowMessage ('Now, what have you done?'); Color := Col; ShowMessage ('You should better buy a new monitor'); end;