![]() Home |
11. Put Code In Units, ComboBoxes Making your code more managible |
![]() Home |
So far all of the Programs in these lessons have been coded entirely in the "Program" .DPR file, but to make your code more Organized and accessable, you may want to use the standard Unit .PAS file as your code containers. You should already be familar with using Units, as Delphi puts it's Forms in a separate Unit, one for each new form. I have introduced a Unit for use in these Lessons called SmallUtils.pas, which only has some Utility functions in it. But if you are creating an API program that has enough code in it to do something useful, your .DPR file will grow to 50 or 100 Kilobytes or more, , so it is helpful to divide up your code into Units. | A Combo Box can be a very useful way to present the user with a list of items to choose from, and it's display will be more compact than a List box. You can also have it so the user can change the selection by typing into it's Edit box. A Combo box gets it's name because it is a "Combination" of an Edit, a List Box and a Button. This combination of controls requires you to use API methods you have seen used for Listboxes, Edits and Buttons. A Combo Box can be Owner Drawn, giving you control of what the Items in the list box and edit look like. There is code here for an Owner Drawn Combo, that will place a small Icon on each list item. The methods for list item drawing can also be used in owner drawn List Boxws and Menus. |
I will create Three Units in this lesson as code containers, and try and divide up the code and place it in a unit to organize it. But first I will give some Information about the syntax of the Delphi (Pascal) unit stucture and methods. Unit Stucture For review, here is information about a Unit File, which you should already have used. . . Delphi's Pascal language supports separately coded and compiled modules of code called "Units". A Delphi Unit (defined in its own .PAS file) consists of types, constants, variables, functions and procedures. A unit file begins with the word unit followed with unit's name (this is the heading section), which is followed by the interface, implementation, initialization, and finalization sections. The initialization and finalization sections are optional. A unit file stucture looks like this: unit Unit1; { Heading }
interface { Public section, available to other units }
uses { List of units used by interface and implementation goes here }
{ Interface definitions go here, no function code allowed }
implementation { Private section, not available to other units }
uses { List of units only for implementation goes here }
{ Implementation definitions and Function Code go here }
initialization
{ Initialization Code is here, optional }
finalization
{ Finalization Code is here, optional }
end.The unit must conclude with the word end followed by a period.Section Definitions - - Heading - Interface - Implementation - Initialization - Finalization -
You should be used to coding in a unit if you have used the default Delphi program creation. But you may have missed some of the factors that units use for program creation, like the public and private nature of the Interface and Implementation sections. The Delphi compilier will NOT place any unused code from a unit (or DPR) in the programs executable file. You can look at the windows.dcu in the Delphi Lib folder, and see that it is more than 500 Kb in size, but if you place windows in your uses clause, you app will NOT automaticaly be over 500 kb, in size, since any unused code in the windows unit is Not compiled in to the program. This means that that you can make some Universal units to be used in many programs, and if you use that unit, only the functions used in your program will be placed in the executable. But you should remember that the Initialization section will always be included, so all of the code in the Initialization section will be added to your program. It may be helpful to limit your Initialization code to only what is efficient.
Three Units as Code Containers The program of this lesson is called "In Units" and will create three units, each having sections of code that are related to the Name of the Unit. There will be almost no code in the program file this time, and the primary code for the operation of this program (hForm1) will be in a unit called "InUnitsU" I will use a Naming convention of adding a U , to the end of the name of a Unit. I call this InUnitsU because it is the unit that has the operational code for the InUnits.DPR program file ( add the U to the DPR program name). There will be a Unit called "ApiFormU", which will have code (like the MakeForm function) in it that can be used in many other programs, not just this one. This ApiFormU unit will be a type of "One Size Fits All" unit, that is meant to be used in many programs, like the SmallUtils Unit. . There is a ComboBoxU.pas unit, which will have the code used by the 4 Combo Boxes created in this example. I will place much of the programs operational code in the InUnitsU, like the MakeApp function (used in the .DPR file) and the control creation code (the MakeControls procedure) and the WindowProc (MessageFunc). I will be using some methods that are similar to what the Delphi Forms Unit does in it's Program file. The InUnits Program file will have two units in it's uses clause, "InUnitsU" and "ApiFormU", , and there will be No functions or procedures defined in the program file this time. There are only 2 lines of code in the program file, the first line calls the MakeApp( ) function in the InUnitsU unit, for window creation in this program. And the next line of code will call the RunMsgLoop procedure in the ApiFormU unit. Let's look at this InUnits program file -
Application.CreateForm(TForm1, Form1); and this may correspond to the InUnits code - The Delphi Program file has the line of code - Application.Run; and this may correspond to the InUnits code -
I will have some "Create" functions in the units, where I use the word "Make" in the function name, like "MakeApp", This MakeApp function is called in the Program file to start the window creation functions for this program. This is the MakeApp function in the InUnitsU.pas file. - function MakeApp: Boolean;
begin
Result := False;
// Returnig False should prevent the message Loop, RunMsgLoop
if SetWinClass('Units Class', @MessageFunc) = Zero then Exit;
{the SetWinClass function simplifies the win Class Register,
You must have a Class Name and a WndProc memory address.
if it fails to Register the win Class, it returns Zero}
hForm1 := MakeForm(DEF, DEF, 546, 351, 'Divide Code Into Units');
{the MakeForm function will create a Main Form window}
if hForm1 = Zero then Exit;
Result := True;
MakeControls;
{the MakeControls procedure creates all
of the Buttons, Labels and ComboBoxes}
end;and for the window that is being created there will be properties in the MakeForm( ) parameters that will have a "Default" value, the coding convention I will use here (used in some of the API functions) is to set that paramter to minus One ( -1 ) to have the Default value used for that parameter. In the MakeForm function -
function MakeForm(Left, Top, Width, Height: Integer; const Caption: String;
WinStyle: Integer = DEF): Integer;
var
Rect1: TRect;
begin
Result := Zero;
{this function will check the Atom1 to see if the wClassEx was
registered and then Create the main Form Window with the parameters}
if Atom1 = Zero then
begin
SetLastError(13); // The data is invalid.
ErrorMsgBox(E_MakeForm1, E_TitleMF);
Exit;
end;
{I use -1 as the Default Value in this WinStyle parameter}
if WinStyle < Zero then
WinStyle := WS_CAPTION or WS_MINIMIZEBOX or WS_SYSMENU or WS_CLIPCHILDREN;
SetRect(Rect1, Zero, Zero,Width,Height);
if not AdjustWindowRect(Rect1, WinStyle,False) then
SetRect(Rect1, Zero, Zero, Width + 6, Height+26);
{if Top is -1 (Default) then the form is centered in the screen vertical}
if Top < Zero then
Top := (GetSystemMetrics(SM_CYSCREEN) shr 1)- ((Rect1.Bottom-Rect1.Top) shr 1);
{if Left is -1 (Default) then the form is centered in the screen horizontal}
if Left < Zero then
Left := (GetSystemMetrics(SM_CXSCREEN) shr 1)- ((Rect1.Right-Rect1.Left) shr 1);
Result := CreateWindow(wClassEx.lpszClassName, PChar(Caption), WinStyle,
Left, Top, Rect1.Right-Rect1.Left,
Rect1.Bottom-Rect1.Top, Zero, Zero, hInstance, nil);
if FirstForm = Zero then
FirstForm := Result;
if Result = Zero then
ErrorMsgBox(E_MakeForm2, E_TitleMF);
end; |
| The window creation methods in this MakeForm function have already been covered in previous Lessons of DelphiZeus. What this MakeForm function tries to do is have a more "General" form creation function, that I can use in many API programs, without writing new code. You could copy and paste this MakeForm function into very many projects and use it without needing to change any of the code there. The Top, Left and WinStyle parameters have "Default Values", , which are used if a negative number (-1) is placed in that parameter. This type of "General" or "One Size Fits All" coding can be helpful to speed up your programming development time. Certianly the Delphi code "Unit" is a great way to organize some "General", "Non-Specific", "Use in any Program" type of code, such as this MakeForm function. You should try and develop your own Units with code that you have commonly used in other projects. |
|
Combo Boxes A Combo Box control gets it's name because it is a "Combination" of controls, an Edit control, a List box control, and with the drop-down styles, a Button control. A Combo Box consists of an Edit control to show it's current selection, a list box control with selections (hidden until needed, if drop-down) and a button control to click to display the hidden list box (if drop down). These Combination controls give the user a list to select from (like a Menu, a group of Radio Buttons or List Box), and the Current Section is shown in the Edit control, where the user can type in their text, to further extend the options for Combo Box selections. This "Three in One" control will require you to use some of the methods you have seen used for Edits, Listboxes and Buttons. A Combo Box is a "Common Control" , Most common controls belong to a Window Class defined in the system's common control library (ComCtl32.DLL). So you will need to call the API fuction InitCommonControls; to make sure the ComCtl32.DLL has been loaded into the system, you will need to add "CommCtrl" to your Uses Clause, for that function. Combo Box Styles - You can use the CreateWindowEx( ) function to create your combo box. In the Style parameter you include the Type of combo box you want. There are three types of combo boxes, "Simple", "Drop Down" and "Drop Down List". One thing to you will need to deal with using either of the "Drop Down" styles, is the Height parameter in CreateWindowEx( ). Unlike controls you have created before, the Height parameter does NOT change the height of the visible Edit control. The Height parameter is for the entire combo box, the edit and the drop-down list box together. The edit control will be auto sized to the font height of the combo box, you will have NO way to change the edit height in the CreateWindow function. When you first try to create a combo box you may set the creation Height to 21, thinking of the size of the edit, and then when you click the drop button, you will NOT see the list box. You will need to set the Height to something larger than the edit, maybe 100, in order to see the list box. The simple combo is an Edit with a List box Always displayed below this edit. If you use a simple combo in a Dialog, it will correctly size and paint it's self. If it is NOT in a dialog it will not size and paint correctly. This simple style is not used anymore, since a Drop Down style will give you better space saving GUI display. The three styles are listed below -
By now you should expect me to tell you to read the Win32 API Help for "Combo Boxes". You can find more about the styles in Help's "Combo Box Types and Styles". In this program I create one of each type of combo box, I only do the Simple combo box to show you what it is, this is almost never used any more, AND if used outside of a templete Dialog box, it will not size and paint correctly. A simple combo box when not in a dialog, will resize it's list box to the Items in the list box (standard list box sizing), but it will NOT resize the combo box to the new list box size. So there can be an area below the always visible list box that is never painted. Note: In the Drop-Down styles, the dropped List Box has a WS_POPUP style flag, so it can go beyond the borders of it's parent, and even beyond the Main Window (Form) borders. Also, if it is at the bottom of the screen, without room to drop below the edit, it will be displayed Above the edit. Using Combo Box Messages - The Combo Box messages are like some of the List Box Messages, and like some of the Edit messages. They have a CB_ prefix for the constant message name. Here is a List of the combo messages used in this program, you may want to look at your Win32 API Help for these - CB_ADDSTRING CB_DELETESTRING CB_FINDSTRING CB_GETCOUNT CB_GETCURSEL CB_GETITEMDATA CB_GETITEMHEIGHT CB_GETLBTEXT CB_INSERTSTRING CB_SELECTSTRING CB_SETCURSEL CB_SETEXTENDEDUI CB_SETITEMDATA CB_SHOWDROPDOWNThese combo box message constants have names that correspond to what that message will do. The CB_ADDSTRING is just like the List box message LB_ADDSTRING, and so is the CB_GETCURSEL like the LB_GETCURSEL. You can look at the code in the in the InUnitsU.pas unit and the ComboBoxU.pas unit to see how to use these messages in the SendMessage( ) function. Like List Boxes, when you create a combo box it is empty, so you send the CB_ADDSTRING message to add a string to it's list box, but this does not put anything in the combo's edit. You can get a combo list box item into the edit with the CB_SETCURSEL message. Using Combo Box Notification Messages - And there are Combo Notification messages sent to it's parent window for events in the combo box, you may want to look at your Win32 API Help for these - CBN_CLOSEUP CBN_DBLCLK CBN_DROPDOWN CBN_EDITCHANGE CBN_EDITUPDATE CBN_ERRSPACE CBN_KILLFOCUS CBN_SELCHANGE CBN_SELENDCANCEL CBN_SELENDOK CBN_SETFOCUSYou have seen the Edits and List boxes send the Notify messages, and the combos are much the same, but have some different messages. The three notify messages I use in this program are CBN_SELENDOK (notifies a user selection change when drop list goes away), the CBN_EDITCHANGE (notifies a type or paste in to the edit), the CBN_DROPDOWN (notifies that the list box is about to be dropped). I use the CBN_SELENDOK instead of the CBN_CLOSEUP or CBN_SELCHANGE message, because it is sent when the selection changes and the drop list box closes. You can look at the code in the in the InUnitsU.pas unit and the ComboBoxU.pas unit to see how to use these notify messages.
You can create two types of Owner Drawn combo boxes, one that has a Fixed non-changing Item height, and one that has a variable Item height, where all of the Items can be different heights. I create the hComboODraw combo box with the CBS_OWNERDRAWFIXED style flag, which will make a fixed height owner drawn combo box. In the Button Edit program, there was an Owner Drawn Button, buttons have only a single thing (Item) to draw, in Owner Drawn List Boxes, Menus, and Combo Boxes, there are lists of Items to paint, each with different text, check boxes or icons. So you will need to test for the Index of the Item and draw what is needed for that Item. When you create an owner drawn combo box, you can include the CBS_HASSTRINGS style, as I did for the code here, or leave it out. If you do NOT include the CBS_HASSTRINGS in the owner draw combo, then the system will NOT accept any item text (string) data from the CB_ADDSTRING message, and will not give any string data in the CB_GETLBTEXT message request. You will need to use your own text data storage (array of string, the Item DATA) for the Item text. In all of the "List" owner drawn controls, there is the Fixed and Variable height options. The WM_MEASUREITEM message will be sent to the control's parent so you can set the Item height. If it is a Fixed Height Item List, the WM_MEASUREITEM is sent ONLY ONCE, when the control is first created, before there are any Items added to it. You will need to set the itemHeight member of the TMeasureItemStruct pointed to by the LParam of the WM_MEASUREITEM message. This TMeasureItemStruct record has the following structure - type
PMeasureItemStruct = ^TMeasureItemStruct;
tagMEASUREITEMSTRUCT = packed record
CtlType: UINT; // type of control, ODT_COMBOBOX in this case
CtlID: UINT; // control ID number
itemID: UINT; // Item Index number, not used for Fixed Height
itemWidth: UINT; // not used in ComboBoxes, only menus
itemHeight: UINT; // you MUST set this for the height
itemData: DWORD; // not used in Fixed height
end;
TMeasureItemStruct = tagMEASUREITEMSTRUCT;
MEASUREITEMSTRUCT = tagMEASUREITEMSTRUCT;You can test the CtlType or the CtlID members if you have more than one Owner Draw control to see which control is being painted. The WM_MEASUREITEM is only sent Once by a Fixed height control, so you will not use the itemID or the itemData members. And you MUST set the itemHeight member when this message is sent. If it is a variable height owner draw control, then the WM_MEASUREITEM will be sent each time the list is painted. And then you will need to test the itemID member for the index number of which Item to Size. The itemWidth member is only used in menus.You can look at the code in the InUnitsU.pas for the MessageFunc and the WM_MEASUREITEM message, it calls the MeasureCombo procedure in the ComboBoxU.pas, and since there is only one Owner Draw control, the MeasureCombo procedure does not test the CtlID member, and just sets the itemHeight member to 18.
Painting this Owner Drawn ComboBox As with all Owner Drawn controls, painting is done in the WM_DRAWITEM message, like the owner drawn button I did in the Button Edit program, you will need to use the TDrawItemStruct in the LParam pointer of the WM_DRAWITEM message. Only this time with a combo box you will need to get the itemID index so you can paint the text and Icon for that list Item. If you look at the WM_DRAWITEM message in the MessageFunc of the InUnitsU.pas file it calls the DrawComboBox( ) procedure in the ComboBoxU.pas file. I use the PDrawItemStruct from the LParam of the WM_DRAWITEM message as the parameter in the DrawComboBox( ) procedure. You might review the members of this PDrawItemStruct. You should look at this DrawComboBox( ) procedure, as with the Owner Drawn Buttons, you will need to test for the ODS_SELECTED in the pDrawItem.itemState, but this time ODS_SELECTED means that the List Item is selected. I need the text to draw for each list Item, which I get using the CB_GETLBTEXT message with the pDrawItem.itemID for the Item Index. I create a color brush and use the FillRect( ) function to paint the background color for each item, using the pDrawItem.rcItem TRect, which will have the rectangle used for each different Item. I test for the ODS_DISABLED state in the pDrawItem.itemState (item is disabled), and set the background brush color to a Button face color. I use the DrawIconEx( ) function to draw the small Icon on the left of the Item. The handle for the small Icon was placed in the Item DATA by the SetItemIcons procedure. I use the CB_GETITEMDATA message to get this small Icon handle in the DATA for each Item.. |
program InUnits;
uses
InUnitsU, ApiFormU;
{$R *.RES}
begin
if MakeApp then // MakeApp in InUnitsU
RunMsgLoop; // RunMsgLoop in ApiFormU
end. |
unit ApiFormU;
{this ApiFormU unit, is an example for a One Size Fits All unit, that
can be used in many other programs, that need an API Main Form creation,
a GetMessage Loop procedure, a simple MakeFont function and a Label}
interface
uses // I have NO Units from this Program in the Uses clause
Windows;
{I want to be able to use this Unit in other programs, so I
want to have only the Windows, Messages, and SmallUtils units used}
const
Zero = 0; // used so much, I made it a const
DEF = -1; // This is my DEFAULT value for Make functions
type
{I will draw text on this hForm1 with a Record's information and call
it a Label. In the WM_PAINT message the PaintLabels procedure
will run through an array of TLabelRec and draw the Text for each Record.
This TLabelRec is used to store all of the info needed to draw a Label}
TLabelRec = Record
Left, Top: Integer; // Position to draw text
TextColor: Cardinal; // label text color
Visible: Boolean; // Will only draw if true
FontHnd: Integer; // font used to draw text
Text: String; // text to draw on Label
end;
TLabelNames = (Label1, Combo2Sel, Combo3Text, Label4, WavFile, BigLabel);
{this Enumerated type TLabelNames has 6 values, a name value for each
of the 6 Labels used in this program, Label Names used in the AryLabel array}
var
AryLabel: Array[TLabelNames] of TLabelRec;
{the AryLabel is an array of TLabelRec with TLabelNames number of
elements, that is used in the PaintLabels procedure, a for loop goes
through the array and draws the Text for each Label. This record array
is a simple way to have "Graphic Controls" that are just drawn on
the main form's DC, and can be changed at run time}
function SetWinClass(ClassName: String; pMessFunc: Pointer;
wcStyle: Integer = CS_PARENTDC or CS_BYTEALIGNCLIENT): Word;
{the SetWinClass function will get the ClassName and Message Function
address, so it can register a new Window System Class for the Form here.
This function MUST be called BEFORE the MakeForm function}
function MakeForm(Left, Top, Width, Height: Integer; Caption: String;
WinStyle: Integer = DEF): Integer;
{the MakeForm function will create the main window (form) using
the class in wClassEx, this wClassEx is filled and registered in
the SetWinClass procedure}
procedure RunMsgLoop(Show: Boolean = True);
{the RunMsgLoop procedure will start the GetMessage loop
to keep this program running}
function MakeFont(Height, Width: Integer; FontName: String;
Bold: Boolean = False; Roman: Boolean = False): Integer;
{the MakeFont function will simplify Font creation, however you will
loose the ability for many font create options}
function MakeButton(Left, Top, Width, Height: Integer; pCaption: PChar;
hParent, ID_Number: Cardinal; hFont: Integer = DEF): Integer;
{the MakeButton function will simplify Button creation, there are NO
style parameters and there is a hFont parameter, to set the font
for the button. This only makes a Push Button with a Tab Stop}
procedure SetLabel(LabelName: TLabelNames; Left, Top: Integer; Text: String;
Color1: Cardinal = Zero; hFont: Integer = Zero);
{the SetLabel procedure takes a TLabelNames and sets the AryLabel array
for that Label with the parameters of this function}
procedure SetLabelCaption(LabelName: TLabelNames; Caption: String);
{the SetLabelCaption procedure is used in the ComboBoxU unit, to change
the Caption of a single Label}
procedure DrawLabels(hDC: Integer; PaintRect: TRect);
{ DrawLabels is called in the WM_PAINT message of the main Forms
MessageFunc in the InUnitsU unit}
implementation
uses
Messages, SmallUtils;
{ just the Messages and SmallUtils units so
I can use the unit in other programs }
const
{I have added these Error Message Text constants for text
to show if a function fails}
E_WinClas1: PChar = 'ERROR - in SetWinClass - Class Name or pMessProc parameter Incorrect';
E_WinClas2: PChar = 'ERROR - in SetWinClass - RegisterClassEx - FAILED';
E_MakeForm1: PChar = 'ERROR - in MakeForm - wClassEx is NOT registered';
E_MakeForm2: PChar = 'ERROR - in MakeForm - CreateWindow - FAILED';
E_MakeBut: PChar = 'ERROR - in MakeEZButton - CreateWindow - FAILED';
E_TitleWinClas: PChar = 'SetWinClass function ERROR';
E_TitleMF: PChar = 'MakeForm function ERROR';
E_TitleButton: PChar = 'MakeButton function ERROR';
var
wClassEx: TWndClassEx;
Atom1: Word = Zero;
FirstForm: Integer = Zero;
procedure ErrorMsgBox(pText, pTitle: PChar);
begin
{this procedure will get the Text for the Last window's Error and
add it to the pText, then display an Error Message Box}
MessageBox(Zero, PChar(pText+#10+SysErrorMessage(GetLastError)),
pTitle, MB_ICONERROR);
end;
procedure SetLabelCaption(LabelName: TLabelNames; Caption: String);
begin
{this procedure will set the text for a single label in the AryLabel
and call for the whole hForm1 window to be Invalidated. This is
ineficient, since you do not need to invalidate the entire window,
but I do not keep a width and height in the TLabelRec,
so I just refresh the whole window}
AryLabel[LabelName].Text := Caption;
InvalidateRect(FirstForm, nil, True);
end;
procedure SetLabel(LabelName: TLabelNames; Left, Top: Integer; Text: String;
Color1: Cardinal = Zero; hFont: Integer = Zero);
begin
{this procedure is used to initialize the AryLabel in the
MakeControls procedure, and will use a LabelName (Index number)
and put Values into the AryLabel[Index] TLabelRec record}
AryLabel[LabelName].Left := Left;
AryLabel[LabelName].Top := Top;
AryLabel[LabelName].TextColor := Color1;
AryLabel[LabelName].Visible := True;
AryLabel[LabelName].FontHnd := hFont;
AryLabel[LabelName].Text := Text;
end;
procedure DrawLabels(hDC: Integer; PaintRect: TRect);
var
LN: TLabelNames;
begin
{this procedure is called in the Main Form's WM_PAINT message
and will draw all of the Labels that are in the AryLabel array}
SelectObject(hDC, GetStockObject(ANSI_VAR_FONT));
SetBkMode(hDC, TRANSPARENT);
{I set the font to ANSI_VAR_FONT and have arranged the labels in the array
so that all labels that use ANSI_VAR_FONT are at the begining of the array
and have a FontHnd of Zero}
for LN := Low(AryLabel) to High(AryLabel) do
begin
if (not AryLabel[LN].Visible) or // will skip drawing if Visible false
(PaintRect.Bottom < AryLabel[LN].Top) or
{if the PaintRect rectangle does not include label then skip draw}
(PaintRect.Right < AryLabel[LN].Left) or
(AryLabel[LN].Text = '') then Continue;
{if there is no Text to draw then skip draw}
if AryLabel[LN].FontHnd > Zero then // only set the font if above Zero
SelectObject(hDC, AryLabel[LN].FontHnd);
SetTextColor(hDC, AryLabel[LN].TextColor);
TextOut(hDC, AryLabel[LN].Left, AryLabel[LN].Top,
PChar(AryLabel[LN].Text), Length(AryLabel[LN].Text));
{this TextOut( ) function is a simple way to get the text on the form}
end;
end;
function MakeFont(Height, Width: Integer; FontName: String;
Bold: Boolean = False; Roman: Boolean = False): Integer;
var
FontLog1: TLogFont;
begin
{this function simplifies Font Creation, you can
create a font with as few as 3 parameters}
ZeroMemory(@FontLog1, SizeOf(FontLog1));
with FontLog1 do
begin
lfHeight := Height;
lfWidth := Width;
if Bold then
lfWeight := 700;
lfCharSet := DEFAULT_CHARSET;
lfOutPrecision := OUT_TT_PRECIS;
if Roman then
lfPitchAndFamily := VARIABLE_PITCH or FF_ROMAN
else
lfPitchAndFamily := VARIABLE_PITCH or FF_SWISS;
StrLCopy(@lfFaceName[Zero], PChar(FontName), 31);
end;
Result := CreateFontIndirect(FontLog1);
end;
function MakeButton(Left, Top, Width, Height: Integer; pCaption: PChar;
hParent, ID_Number: Cardinal; hFont: Integer = DEF): Integer;
begin
{this function simplifies button creation by including a Font
parameter, which is used to set the button's font}
case hFont of
Zero: hFont := GetStockObject(SYSTEM_FONT);
{zero or any number that is not -1, -2, or a system font handle,
will get the standard System Font for the button}
DEF: hFont := GetStockObject(ANSI_VAR_FONT);
-2: hFont := GetStockObject(ANSI_FIXED_FONT);
{ -1 and -2 will get Var and Fixed Stock fonts}
end;
Result := CreateWindow('Button', pCaption, WS_VISIBLE or WS_CHILD
or BS_PUSHBUTTON or BS_TEXT or WS_TABSTOP, Left, Top,
Width, Height, hParent, ID_Number, hInstance, nil);
{there is NO way to adjust the style, so this only makes a BS_PUSHBUTTON}
if Result = Zero then
begin
ErrorMsgBox(E_MakeBut, E_TitleButton);
Exit;
end;
SendMessage(Result, WM_SETFONT, hFont, Zero);
end;
function SetWinClass(ClassName: String; pMessFunc: Pointer;
wcStyle: Integer = CS_PARENTDC or CS_BYTEALIGNCLIENT): Word;
begin
{this function will set the wClassEx record and regsiter a new Class.
You MUST call this function BEFORE you can call the MakeForm function}
Result := Zero;
{test the parameters}
if (Length(ClassName) < 2) or (pMessFunc = nil) then
begin
SetLastError(13); // The data is invalid.
ErrorMsgBox(E_WinClas1, E_TitleWinClas);
{the ErrorMsgBox procedure takes the string constants
and shows an Error Message Box}
Exit;
end;
{I have 3 wcStyle preSets, , a CS_PARENTDC or CS_BYTEALIGNCLIENT if No
parameter is set and a -1 and -2 alternates}
if wcStyle = DEF then
wcStyle := CS_PARENTDC
else
if wcStyle = -2 then
wcStyle := CS_PARENTDC or CS_HREDRAW or CS_VREDRAW or CS_BYTEALIGNCLIENT
else
if wcStyle < -2 then
wcStyle := Zero;
{the wClassEx was Filled with Zeros in the initialization}
with wClassEx do
begin
cbSize := SizeOf(wClassEx);
Style := wcStyle;
hInstance := SysInit.hInstance;
hIcon := LoadIcon(hInstance,'MAINICON');
lpfnWndProc := pMessFunc;
hbrBackground := COLOR_BTNFACE+1;
GetMem(lpszClassName, Length(ClassName)+1);
StrCopy(lpszClassName, PChar(ClassName));
hCursor := LoadCursor(Zero, IDC_ARROW);
end;
Atom1 := RegisterClassEx(wClassEx); // Zero in Atom1 means Failure
if Atom1 = Zero then
begin
ErrorMsgBox(E_WinClas2, E_TitleWinClas);
Exit;
end;
Result := Atom1;
end;
function MakeForm(Left, Top, Width, Height: Integer; Caption: String;
WinStyle: Integer = DEF): Integer;
var
Rect1: TRect;
begin
Result := Zero;
{this function will check the Atom1 to see if the wClassEx was
registered and then Create the main Form Window with the parameters}
if Atom1 = Zero then
begin
SetLastError(13); // The data is invalid.
ErrorMsgBox(E_MakeForm1, E_TitleMF);
Exit;
end;
{I use -1 as the Default Value in this WinStyle parameter}
if WinStyle < Zero then
WinStyle := WS_CAPTION or WS_MINIMIZEBOX or WS_SYSMENU or WS_CLIPCHILDREN;
SetRect(Rect1, Zero, Zero,Width,Height);
if not AdjustWindowRect(Rect1, WinStyle,False) then
SetRect(Rect1, Zero, Zero, Width + 6, Height+26);
{if Top is -1 (Default) then the form is centered in the screen vertical}
if Top < Zero then
Top := (GetSystemMetrics(SM_CYSCREEN) shr 1)- ((Rect1.Bottom-Rect1.Top) shr 1);
{if Left is -1 (Default) then the form is centered in the screen horizontal}
if Left < Zero then
Left := (GetSystemMetrics(SM_CXSCREEN) shr 1)- ((Rect1.Right-Rect1.Left) shr 1);
Result := CreateWindow(wClassEx.lpszClassName, PChar(Caption), WinStyle,
Left, Top, Rect1.Right-Rect1.Left,
Rect1.Bottom-Rect1.Top, Zero, Zero, hInstance, nil);
if FirstForm = Zero then
FirstForm := Result;
if Result = Zero then
ErrorMsgBox(E_MakeForm2, E_TitleMF);
end;
procedure RunMsgLoop(Show: Boolean = True);
var
MainMsg: TMSG;
begin
if FirstForm = Zero then Exit;
{this RunMsgLoop procedure will run the GetMessage Loop to keep
this program running, I have included a Show parameter, even though
it is not used in this program}
if Show then
ShowWindow(FirstForm, SW_SHOWDEFAULT);
while GetMessage(MainMsg,Zero,Zero,Zero) do
begin
if not IsDialogMessage(FirstForm, MainMsg) then
begin
TranslateMessage(MainMsg);
DispatchMessage(MainMsg);
end;
end;
end;
initialization
{I will fill the wClassEx and AryLabel with Zeros}
ZeroMemory(@wClassEx, SizeOf(wClassEx));
ZeroMemory(@AryLabel, SizeOf(AryLabel));
finalization
if wClassEx.lpszClassName <> nil then
FreeMem(wClassEx.lpszClassName);
end. |
| I have a single windowed control creation function in this unit, MakeButton( ), which makes a Button, but it does not have many creation options. If you include more creation options, then the code size for that creation function will increase, so if you try and place ALL of the posible creation options, there will be much code in the function that is not used, unless you call for each availible option. Since I know the API code to change the style flags in the CreateWindow( ) function, I can make a simple MakeButton function, and if I need more options, I can just use the CreateWindow function.
This Unit has the operational code for this program, there is only one public interface function, MakeApp in this unit. This is called in the .DPR file to start the window and font creations for this program. The code used for creation of the main Window (Form) in now in the ApiFormU.pas unit. The procedure MakeControls is where the fonts and controls are created and initilized for this program. First, there are 2 fonts created and then four Buttons are created, I create the Buttons with the MakeButton function, in the ApiFormU unit. Next the Combo Boxs are created by calling the MakeCombo function in the ComboBoxU.pas unit. Next I make 6 "Labels", by setting the the data in the AryLabel with the SetLabel procedure in the ApiFormU unit. The Window Proc (MessageFunc) for this program in in this unit, the methods used in this MessageFunc function are like some you have seen before in previous lessons, except there is the addition of the Combo Box Messages. In the ChangeLabels procedure, there is code that will set the members of the TLabelRec record, in the AryLabel array in order to change the way the labels are painted. The ComboODrawMsg( ) procedure is like the IsComboMsg( ) function in the ComboBoxU unit, and will process the CBN_SELENDOK combo message, and display the WavFile Label if the file extention is .wav. If you look in the constants you will see the RectInv const, which is a TRect, you can declare a Record const, by giving all of the record members a value. This RectInv is used in the InvalidateRect(hForm1, @RectInv, True); to invalidate just the Form's area that has the WavFile Label in it. You should notice that I have moved the two DeleteObject( ) functions for the two fonts created, from the WM_DESTROY message to the finalization section of this unit. That way the fonts are deleted even if the WM_DESTROY is not processed. see comments in code for more info |
unit InUnitsU;
{this unit has the code required to set up and run this program
this is the "Main Unit" for this application, with a single public
function called MakeApp . This function with code to register the
windows class, to Create the main Window and controls needed}
interface
var
hForm1: Integer = 0; // handle of Main Window (Form)
FontCombo: Integer = 0;
txBuffer: Array[0..63] of Char;
function MakeApp: Boolean;
{the MakeApp function will call some functions to create the windows
and controls for this Application.
If there is a creation error, it returns False}
implementation
uses
Windows, Messages, CommCtrl, ComboBoxU, ApiFormU, SmallUtils;
const
{I use ID number constants for the Four buttons, instead of handles}
ID_ExitBut = 1000;
ID_LChangeBut = 1001;
ID_MovWinBut = 1002;
ID_DropBut = 1003;
{the RectInv TRect is the rectangle constant for the
rectangle used for the WavFileLabel Label Invalidate}
RectInv: TRect = (left: 50; Top: 287; Right: 230; Bottom: 307);
{you can declare Record constants by giving ALL of it's members values}
var
FontLarge: Integer = Zero;
procedure ChangeLabels;
begin
{this procedure will change and move several Labels
just by changing the values in the Records in AryLabel}
if AryLabel[Label1].Visible then
begin
AryLabel[BigLabel].Left := 190;
AryLabel[BigLabel].Top := 100;
AryLabel[BigLabel].TextColor := $FF00FF;
AryLabel[Label4].Text := 'This is new text for Label Four';
end else
begin
AryLabel[BigLabel].Left := 290;
AryLabel[BigLabel].Top := 250;
AryLabel[BigLabel].TextColor := $22A7FF;
AryLabel[Label4].Text := 'Other words in Label 4';
end;
AryLabel[Label1].Visible := not AryLabel[Label1].Visible;
InvalidateRect(hForm1, nil, True);
// IvalidateRect will redraw all of the Form's DC and the labels
end;
procedure ComboODrawMsg(wParam1, LParam1: Integer);
var
doInval: Boolean;
begin
{I have placed procedures in both this InUnitsU and the ComboBoxU unit
for combo box message handling, depending on your Code organization,
you can place the code for a control's message handling in that control's
unit or in this unit. See the IsComboMsg( ) function in the ComboBoxU unit}
if HIWORD(wParam1) = CBN_SELENDOK then
begin
{combo boxs use Messages, like List Boxes do, to get selection and
Item's Text, the CB_GETCURSEL get's the selection, and
CB_GETLBTEXT will get an Item's text}
if SendMessage(LParam1, CB_GETLBTEXT, SendMessage(LParam1,
CB_GETCURSEL, Zero, Zero), Integer(@txBuffer)) <> CB_ERR then
begin
doInval := AryLabel[WavFile].Visible;
{if the user picks a WAV file in this combo, then I will show the WavFile Label}
if GetFileExt(txBuffer) = '.wav' then // test the text file extention
AryLabel[WavFile].Visible := True // if Wav show Label lnWavFile
else
AryLabel[WavFile].Visible := false;
if doInval <> AryLabel[WavFile].Visible then
InvalidateRect(hForm1, @RectInv, True);
{test the doInval with AryLabel[lnWavFile].Visible to
see if you need to Invalidate}
end;
end;
end;
{as in Previous Lessons, messages are handled in this MessageFunc.
There are no new methods here, except that functions from other
Units are called.}
function MessageFunc(hWnd,Msg,wParam,lParam:Integer):Integer; stdcall;
var
PaintS: TPaintStruct;
Rect1, workRect: TRect;
begin
{some of these messages will call functions or procedures in other Units}
case Msg of
WM_PAINT:
begin
BeginPaint(hWnd, PaintS);
TextOut(PaintS.hDC, 4,120, 'Label0 with text defaults' , 26);
DrawLabels(PaintS.hDC, PaintS.rcPaint); { in ApiFormU unit, DrawLabels
runs a loop through AryLabel to draw all of the Labels}
EndPaint(hWnd,PaintS);
Result := Zero;
Exit;
end;
WM_COMMAND: if LOWORD(wParam) = ID_ExitBut then
PostMessage(hForm1, WM_CLOSE, Zero, Zero)
else if LOWORD(wParam) = ID_LChangeBut then
ChangeLabels
else if LOWORD(wParam) = ID_DropBut then
begin
{this Drop Combo button click will show hCombo3 List Box}
{the CB_SELECTSTRING will search the Combo Items for a match for text}
SendMessage(hCombo2, CB_SELECTSTRING, Zero, Integer(PChar('MI')));
SendMessage(hCombo3, CB_SHOWDROPDOWN, 1, Zero);
{the CB_SHOWDROPDOWN will make the Combo List Box visible}
EnableWindow(hCombo2, not IsWindowEnabled(hCombo2));
EnableWindow(hComboODraw, not IsWindowEnabled(hComboODraw));
{hComboODraw is disabled to show the drawing of disabled Owner Draw}
SetLabelCaption(Label4, 'Click Drop Combo Button again to Enable hCombo2');
end
else if LOWORD(wParam) = ID_MovWinBut then
begin
{this Move Window button will put this form at the bottom of the work area
and drop down the hComboODraw combo box, so you can see that the List Box
will be ABOVE the Combo Edit and not below it, since there is no room below}
GetWindowRect(hWnd, Rect1);
if SystemParametersInfo(SPI_GETWORKAREA, Zero, @workRect,Zero) then
MoveWindow(hWnd, Rect1.Left, workRect.Bottom - (Rect1.bottom -Rect1.Top),
Rect1.right - Rect1.left,Rect1.Bottom - Rect1.Top, True);
SendMessage(hComboODraw, CB_SHOWDROPDOWN, 1, Zero);
{the hComboODraw will show it's Drop Down List box when it get's the
CB_SHOWDROPDOWN message, even if it is Disabled}
end // the IsComboMsg function will test the LParam for a Combo Handle
else if IsComboMsg(WParam, LParam) then
if lParam = hComboODraw then
ComboODrawMsg(wParam, LParam);
{see the IsComboMsg function in the ComboBoxU unit and the ComboODrawMsg above}
WM_DESTROY: PostQuitMessage(Zero);
{I have moved the DeleteObject( ) to the finalization clause}
WM_MEASUREITEM:
begin
{Since there is Only one Owner draw control, I do not test the wParam}
MeasureCombo(PMeasureItemStruct(LParam));
{the MeasureCombo procedure is in the ComboBoxU unit for the hComboODraw}
Result := 1;
Exit; // Do NOT call DefWindowProc
end;
WM_DRAWITEM:
begin
DrawComboBox(PDrawItemStruct(LParam));
{the DrawComboBox procedure is in the ComboBoxU unit for the Owner Draw Combo}
Result := 1;
Exit;
end;
end; // case
Result := DefWindowProc(hWnd,Msg,wParam,lParam);
end;
{the MakeControls procedure has the code to create and set up all of the
buttons, listboxs, comboBoxes, and labels on this form}
procedure MakeControls;
var
ListStr: String;
ItemHeight: Integer;
begin
{this procedure will create Fonts and the controls on the main form}
FontLarge := MakeFont(-30, 14, 'Comic Sans MS', True);
{MakeFont has only 4 parameters for font creation}
FontCombo := MakeFont(-14, 6, 'Arial');
{the Four Buttons are created below with ID numbers, not handles.
The MakeButton function is in the ApiFormU unit,
it creates a Button and sets it's font}
MakeButton(442,292,88,40, 'EXIT', hForm1, ID_ExitBut, FontLarge);
MakeButton(8,146,88,26, 'Change Labels', hForm1, ID_LChangeBut);
MakeButton(8,184,80,26, 'Drop Combo', hForm1, ID_DropBut);
MakeButton(8,228,108,26, 'Move Window', hForm1, ID_MovWinBut, Zero);
InitCommonControls; {InitCommonControls is neccessary if you
have any Comon Controls like Combo Boxes}
{ListStr is for ListItems in the combo box, it must have a #255 in front,
and an extra #0 at the end, in order to work}
ListStr := #255'Simple Combo'#0'First Item'#0'Another Item'#0'More Here'#0+
'the list goes on'#0'Five Items'#0'Sixth and last Item'#0;
{the ComboBoxU unit has functions to deal with Combo Boxes
this MakeComboBox has a ListItems String Parameter, which uses a #0 delimited
String to place several Items in the Combo Box with just one string}
hCombo1 := MakeComboBox(6,4, 128, 99, ListStr, WS_VISIBLE or WS_CHILD or
CBS_SIMPLE or WS_CLIPSIBLINGS or WS_TABSTOP or WS_VSCROLL or CBS_SORT);
{just for an Example, hCombo1 is a Simple Combo, CBS_SIMPLE, which will only
correctly size and paint itself on a Dialog Box.
Simple Combos are not used anymore}
{hCombo1 has the CBS_SORT style flag, which will sort all
of the Items in the List Box alphabeticly}
{hCombo2 is a CBS_DROPDOWNLIST with no Edit Input}
hCombo2 := MakeComboBox(150,26, 132, 108, CB2Items, WS_VISIBLE or WS_CHILD or
CBS_DROPDOWNLIST or WS_CLIPSIBLINGS or WS_TABSTOP or WS_VSCROLL);
{I have included the WS_VSCROLL style in ALL of these Combo
Boxes so they will have a Scroll Bar if there are more Items
in the list box than it can show}
SendMessage(hCombo2,WM_SETFONT,GetStockObject(ANSI_VAR_FONT), Zero);
{Using the CB_SETEXTENDEDUI message will change the Keyboard response for a
Combo Box. By default, the F4 key opens or closes the combo list and
the DOWN ARROW changes the current selection. In the extended user interface,
the F4 key is disabled and the DOWN ARROW key opens the drop-down list}
SendMessage(hCombo2, CB_SETEXTENDEDUI, 1, Zero);
ListStr := #255'C:\Windows\'#0'C:\My Documents\'#0'C:\Some Folder\'#0;
{hCombo3 is a CBS_DROPDOWN and has Edit Input}
hCombo3 := MakeComboBox(300,26, 213, 128, ListStr); // uses Default Style
ItemHeight := SendMessage(hCombo3,CB_GETITEMHEIGHT, Zero, Zero);
SendMessage(hCombo3,CB_ADDSTRING, Zero, Integer(PChar(Int2Str(ItemHeight))));
ListStr := #255'C:\aText.txt'#0'C:\aFont.ttf'#0'C:\aSound.wav'#0'C:\aDoc.doc'+
#0'C:\aWebPage.htm'#0'C:\aBitmap.bmp'#0'C:\aHelp.hlp'#0'C:\aWav.wav'#0;
{this ListStr is all file names for the owner drawn}
{the hComboODraw is an Owner Drawn Combo Box with the CBS_OWNERDRAWFIXED style,
which is Drawn in the DrawComboBox procedure, I draw small Icons with the text}
hComboODraw := MakeComboBox(46,306, 173, 128, ListStr, WS_VISIBLE or WS_CHILD or
CBS_DROPDOWNLIST or WS_CLIPSIBLINGS or WS_TABSTOP or WS_VSCROLL or
CBS_HASSTRINGS or CBS_OWNERDRAWFIXED);
ListStr := '';
{I will use small file icons in the owner drawn hComboODraw combo box.
The SetItemIcons procedure in the ComboBoxU unit finds and sets the Item data
to the shell small icon handle}
SetItemIcons;
{the next six SetLabel will make 6 Labels (Text on Form)}
SetLabel(Label1, 150, 72, 'Label One', $AF3300);
SetLabel(Combo2Sel, 154, 4, 'Combo2 Selection');
{you should place all of the Labels with the ANSI_VAR_FONT at the begining
of this Array Initialization, Label1 and Label2 will have ANSI_VAR_FONT}
SetLabel(Combo3Text, 304, 4, 'C:\Windows\', Zero, GetStockObject(ANSI_FIXED_FONT));
SetLabel(Label4, 100, 182, 'Label Four', $FF, FontCombo);
SetLabel(WavFile, 50, 287, 'Combo now has a .WAV file', $AC0091, FontCombo);
SetLabel(BigLabel, 240, 200, 'Label Six moves', $009900, FontLarge);
AryLabel[WavFile].Visible := False; // set lnWavFile to not visible
end;
function MakeApp: Boolean;
begin
Result := False; // Returnig False should prevent the message Loop, RunMsgLoop
if SetWinClass('Units Class', @MessageFunc) = Zero then Exit;
{the SetWinClass function simplifies the win Class Register,
You must have a Class Name and a WndProc memory address.
if it fails to Register the win Class, it returns Zero}
hForm1 := MakeForm(DEF, DEF, 546, 351, 'Divide Code Into Units');
if hForm1 = Zero then Exit;
{the MakeForm function will create a Main Form window}
Result := True;
MakeControls; // finish with the control creation
end;
initialization
// need to have empty initialization, for the finalization included
finalization
{I have moved the clean Up from the WM_DESTROY message
to this finalization, even if you call Halt this
finalization will be executed}
DeleteObject(FontLarge);
DeleteObject(FontCombo);
end.
|
| I have placed code to handle the Combo Box notify message of it's parent WM_COMMAND message, in this unit and the ComboBoxU unit. To show you that you will need to have your own "Code Organization" system when you use units, and place your code in the unit that makes it better for you.
Code for ComboBoxU.pas This unit will have much of the code needed for the combo boxes in this program. It has the MakeComboBox function for the combo box creation, and the IsComboMsg function to handle three of the combo boxes messages. I have placed the fourth combo box message handeling function in the InUnitsU unit, to show how you can place them where you feel your personal code organization will benifit. The three procedures MeasureCombo, DrawComboBox, and SetItemIcons are all for the Owner Draw Combo Box, which I talked about in the Owner Drawn Combo Box section above. The MakeComboBox( ) function has a Default for the WinStyle parameter, you may want to change the default style to whatever combo style that you mostly want to use. To make adding combo list Items easier durring the combo creation, I have the ListItems parameter as a #0 delimited string. I use the #0 delimiter (the API also used #0 delimiter, for some shell functions), because a system string, PChar, is null (#0) terminated. I can just give a pointer to the begining character of that string section, and the system will automatically stop the character read of the next #0. You will need to have a #0 at the end of the ListItems string to signal the End of the text data in in this string. I have placed a #255 charater at the begining of the ListItems string, for a "Safety" test, to make sure you pass a ListItems type of #0 delimited string to this function. You can leave out this test for #255 and it will work just fine.
I have found that the charater numbers above 128 (like #255) do not have keyboard keys for them (in english anyway) and can be used as delimiters, safety tests, or numeric data. You could use normal characters or characters above 128 (#254) at the begining of this ListItems string to set options like a sorted combo box. You should look at the I have included but commented out, code that will re-size a simple combo outside of a Dialog box. see comments in code for more info |
unit ComboBoxU;
{this unit will have the functions, and variables used for the Combo Boxes}
{I do NOT try and make this a Unit to be used in other Programs, just this
program. The IsComboMsg function will do things ONLY needed by this
program. However, you could make some changes to make this a more
Universal, "One Size Fits All" Combo Box Unit, to be
used in other programs needing Combo Boxes}
interface
uses Windows;
const
CB2Items = #255'At Top'#0'Next Item'#0'Middle'#0'Lower'#0'Lowest'#0'Down'#0+
'Way Down'#0'Way way Down'#0'Time -'#0; // needs #0 at end
var
hCombo1, hCombo2, hCombo3, hComboODraw: Integer;
{I have placed the Combo Box Handles in this unit,
but you could also place them in the InUnitsU unit, if you are
tring to make a One Size Fits All, Combo Unit}
{this MakeComboBox function will create a Combo Box
and set the text for it's Items with the #0 delimited string ListItems}
function MakeComboBox(Left, Top, Width, Height: Cardinal;
ListItems: String; WinStyle: Integer = -1): Integer;
{this IsComboMsg function is called in the WM_COMMAND message of the
MessageFunc function in the InUnitsU, it will handle the notification
messages from the Combo Boxes}
function IsComboMsg(WParam1, LParam1: Integer): Boolean;
{there is One Owner Drawn Combo Box, hComboODraw, the MeasureCombo
procedure is called in the WM_MEASUREITEM message of the MessageFunc
function and will give the Item Height for that Combo Box}
procedure MeasureCombo(pMeasureI: PMeasureItemStruct);
{the DrawComboBox procedure is called in the WM_DRAWITEM message
of the MessageFunc function, and draws the Items in the
owner drawn Combo Box}
procedure DrawComboBox(pDrawItem: PDrawItemStruct);
{I use the system's file small Icons in the DrawComboBox
for the file names of the items. So I need to get the small Icon
Handles, and put them in the Item Data for each Item in the
Owner Drawn Combo Box. The SetItemIcons procedure will get
these Icon handles}
procedure SetItemIcons;
implementation
uses
Messages, ShellApi, InUnitsU, ApiFormU, SmallUtils;
{ShellApi is needed for the SHGetFileInfo function to get
Icons in the DrawComboBox procedure}
const
ID_Combo0 = 300;
var
ComboNumber: Integer = Zero;
{ComboNumber will record the number of Combo Boxes created
in the MakeComboBox function}
procedure SetItemIcons;
var
Count, Index: Integer;
SHResult: Cardinal;
ShInfo1: TSHFILEINFO;
begin
{the CB_GETCOUNT message will get the number of Items in the combo's list box}
Count := SendMessage(hComboODraw, CB_GETCOUNT, Zero, Zero);
{I need to loop through All of the Combo box Items, so I get
the Count of Items in that combo box}
if Count < 1 then Exit;
for Index := Zero to Count - 1 do
begin
if SendMessage(hComboODraw, CB_GETLBTEXT, Index,
Integer(@txBuffer)) = CB_ERR then Continue;
{for a File list it is often good to draw the File Icons next to the file name.
I use the SHGetFileInfo function to get the handle of the Small Icon
associated with that file extention, if you use the SHGFI_USEFILEATTRIBUTES
flag, it will get the default icon, even if the file does NOT exist, as
the file names in this combo box do not exist}
SHResult := SHGetFileInfo(txBuffer, FILE_ATTRIBUTE_NORMAL,
ShInfo1, SizeOf(TSHFILEINFO),
SHGFI_ICON or SHGFI_SMALLICON or SHGFI_USEFILEATTRIBUTES);
{the ShInfo1.hIcon will have the small Icon's Handle,
in the DrawComboBox procedure}
if SHResult = Zero then
ShInfo1.hIcon := Zero;
SendMessage(hComboODraw, CB_SETITEMDATA, Index, ShInfo1.hIcon);
{Every Item in ListBoxes and ComboBoxes have a 32-Bit value as Item Data,
that can be set and retrieved with messages, CB_SETITEMDATA, , CB_GETITEMDATA.
I set the Item Data to a value of the small icon handle}
end; // for loop
end;
function IsComboMsg(WParam1, LParam1: Integer): Boolean;
var
Item1: Integer;
TimeRec: TSystemTime;
TimeStr: String;
AM: Boolean;
begin
{this IsComboMsg will handle the Notification messages in the MessageFunc
function of the InUnitsU unit, I have placed it here in the ComboBoxU
because these messages are for user input for the Combo Boxes, but you could
place it in another unit, if it offers better code organization for you}
Result := True; // a True Result will allow the WM_COMMAND message to continue
if LParam1 = hCombo1 then // test the LParam1 for the combo handle
begin
Result := False; // set Result to False to end the WM_COMMAMD tests
{the HiWord of the WParam is the notification message for a Combp Box
the CBN_SELENDOK notification is sent after the Selection has changed}
if HIWORD(wParam1) = CBN_SELENDOK then
{using SendMessage with the CB_GETCURSEL message will get the current
Selection of the Combo Box, the CB_GETLBTEXT will get the Text of an Item
in the List Box of the Combo}
if SendMessage(LParam1, CB_GETLBTEXT, SendMessage(LParam1,
CB_GETCURSEL, Zero, Zero), Cardinal(@txBuffer)) <> CB_ERR then
begin
SendMessage(hCombo3, CB_ADDSTRING, Zero, Integer(@txBuffer));
{using SendMessage with the CB_ADDSTRING message, will put
a new Item in hCombo3 list box}
AryLabel[Label1].TextColor := $6000B7;
SetLabelCaption(Label1, 'Look in hCombo3 for "'+txBuffer+'"');
end;
end else
if LParam1 = hCombo2 then
begin
Result := False;
if HIWORD(wParam1) = CBN_SELENDOK then
begin
if SendMessage(hCombo2, CB_GETLBTEXT, SendMessage(hCombo2,
CB_GETCURSEL, Zero, Zero), Integer(@txBuffer)) <> CB_ERR then
SetLabelCaption(Combo2Sel, txBuffer);
end else
if HIWORD(wParam1) = CBN_DROPDOWN then
begin
{the CBN_DROPDOWN notify is sent BEFORE the List Box is shown, so you can
test or change the Items of the Combo Box before the List Box is shown}
GetLocalTime(TimeRec);
{before this Combo drops down the list box, I update the "Time" item
with the current local time and create the TimeStr}
if TimeRec.wHour > 12 then
begin
AM := False;
TimeRec.wHour := TimeRec.wHour - 12;
end else
AM := True;
TimeStr := Int2Str(TimeRec.wMinute);
if Length(TimeStr) = 1 then
TimeStr := '0'+TimeStr;
TimeStr := Int2Str(TimeRec.wHour)+' :'+ TimeStr +
' '+Int2Str(TimeRec.wSecond)+' Sec';
if AM then
TimeStr := TimeStr + ' AM'
else
TimeStr := TimeStr + ' PM';
Item1 := SendMessage(LParam1, CB_GETCURSEL, Zero, Zero);
{there is no Combo message to "Change" or modify a Combo Item, so
you will have to Delete the Item with CB_DELETESTRING and then
Insert a New Item with CB_INSERTSTRING, in order to Change an Item}
SendMessage(LParam1, CB_DELETESTRING, 8, Zero);
SendMessage(LParam1, CB_INSERTSTRING, 8, Integer(PChar(TimeStr)));
if Item1 = 8 then
SendMessage(LParam1, CB_SETCURSEL, 8, Zero);
{when you Delete an Item that has the Selection, there will be NO SELECTION,
so you will need to use the CB_SETCURSEL message to reset the selection}
end;
end else
if lParam1 = hCombo3 then
begin
Result := False;
{the CBN_EDITCHANGE notification is sent when the text in the Edit Box
of the Combo has been changed by Keyboard or user Paste, but Not by
Combo box selection change}
if HIWORD(wParam1) = CBN_EDITCHANGE then
begin
if SendMessage(hCombo3, WM_GETTEXT, 64, Integer(@txBuffer)) > Zero then
SetLabelCaption(Combo3Text, txBuffer);
{the Text in Label Combo3Text will change whenever
you type into the Edit Box}
end else
if HIWORD(wParam1) = CBN_SELENDOK then
begin
if SendMessage(hCombo3, CB_GETLBTEXT, SendMessage(hCombo3,
CB_GETCURSEL, Zero, Zero), Integer(@txBuffer)) <> CB_ERR then
SetLabelCaption(Combo3Text, txBuffer);
end;
end;
end;
procedure MeasureCombo(pMeasureI: PMeasureItemStruct);
begin
pMeasureI^.itemHeight := 18;
{Since I will draw small Icons (16x16) into the Owner Draw Combo Box
I will set the itemHeight to 18, since this is a CBS_OWNERDRAWFIXED
Combo (fixed size items), this WM_MEASUREITEM message is only sent Once,
when the Combo Box is created}
end;
procedure DrawComboBox(pDrawItem: PDrawItemStruct);
var
BrushC: Integer;
begin
{This procedure will draw all of the Items in the combo box.
This is called in the main form's WM_DRAWITEM message.
the PDrawItemStruct is in the LParam of that message and has
the information used to draw an Item.}
if pDrawItem.itemAction and ODA_FOCUS <> Zero then exit;
// I do not have any Focus drawing
{first I get the text of the item being painted}
if SendMessage(pDrawItem.hWndItem, CB_GETLBTEXT, pDrawItem.itemID,
Integer(@txBuffer)) = CB_ERR then
begin
{if there is no Text in the Item then I place a ? in it.
This is not nessary, but shows how to change the text to
whatever you want}
txBuffer[Zero] := '?';
txBuffer[1] := #0;
end;
{the pDrawItem.hWndItem has the Handle of the Combo Box being Painted
the pDrawItem.itemID is the Item Index for the item being painted}
{the pDrawItem.itemState has the State of the Item, I test for the
ODS_SELECTED and the ODS_DISABLED states here, there are other states
but I do not use them here}
if (pDrawItem.itemState and ODS_SELECTED) <> Zero then
begin
{if the Item is selected, the Rect is filled with the system select color}
FillRect(pDrawItem.hDC, pDrawItem.rcItem, GetSysColorBrush(COLOR_HIGHLIGHT));
SetTextColor(pDrawItem.hDC,GetSysColor(COLOR_HighLightText));
end else
if (pDrawItem.itemState and ODS_DISABLED) <> Zero then
begin
{if the Item is Disabled, the Rect is filled with the system button color}
FillRect(pDrawItem.hDC, pDrawItem.rcItem, GetSysColorBrush(COLOR_3DFACE));
SetTextColor(pDrawItem.hDC, GetSysColor(COLOR_GRAYTEXT));
{text color is set to system Grey Text , diabled, grey color}
end else
begin
{the file extention for the file name is tested and if it is a WAV file
the fill brush color is a light red color, normal is a green color}
if UpperCase(GetFileExt(txBuffer)) = '.WAV' then
BrushC := CreateSolidBrush($DFCFFF) else
BrushC := CreateSolidBrush($D9FFC9);
FillRect(pDrawItem.hDC, pDrawItem.rcItem, BrushC);
DeleteObject(BrushC);
SetTextColor(pDrawItem.hDC,$009000A0);
end;
{DrawIconEx will draw a small icon without resizing it if you have the
DI_NORMAL flag set.}
DrawIconEx(pDrawItem.hDC,3,pDrawItem.rcItem.Top+1,
SendMessage(pDrawItem.hWndItem, CB_GETITEMDATA, pDrawItem.itemID, Zero),
Zero, Zero, Zero, Zero, DI_NORMAL);
{I use the SendMessage with CB_GETITEMDATA, to get this Item Data
that has the handle of the small Icon}
SelectObject(pDrawItem.hDC, FontCombo);
SetBkMode(pDrawItem.hDC, Transparent);
{set the font and Background Mode for text drawing and draw the Text}
TextOut(pDrawItem.hDC,22,pDrawItem.rcItem.Top+1, txBuffer, PCharLength(txBuffer));
end;
function MakeComboBox(Left, Top, Width, Height: Cardinal;
ListItems: String; WinStyle: Integer = DEF): Integer;
var {WinStyle defaults to Def, -1 }
Lim: Integer;
pGet: PChar;
{the 4 variables below are used to get the correct Simple Combo Size}
//hChild: Integer;
//pnt: TPoint;
//eRect, LBRect: TRect;
begin
{a -1 (DEF) in the WinStyle will get the Default style Flags}
if WinStyle < Zero then
WinStyle := WS_VISIBLE or WS_CHILD or CBS_DROPDOWN or
WS_CLIPSIBLINGS or WS_TABSTOP or WS_VSCROLL;
Result := CreateWindow('COMBOBOX',PChar('cb'+Int2Str(ComboNumber)),
WinStyle,Left,Top,Width,Height,hForm1,ID_Combo0+ ComboNumber,hInstance,nil);
{the ID of the Combo Box is set to the ID_Combo0+ ComboNumber, although I
do not use these ID's in this program}
if Result = Zero then Exit;
Inc(ComboNumber);
SendMessage(Result,WM_SETFONT,GetStockObject(ANSI_FIXED_FONT), Zero);
{if you create a Simple Combo Box, that is NOT in a Dialog window,
it WILL NOT size and paint correctly, I have commented out the
following code, that will correctly "Size" a simple combo box.
That way you can see that the simple Combo, hCombo1, will have a
space below that does NOT get any painting, because the List
Box is sized to the Items in it, but the combo box is not}
{you can remove the Comment marks of the code below to get correct
Simple Combo sizing, however, simple combos are not used anymore}
{if (WinStyle and CBS_SIMPLE) <> Zero then
begin
pnt.x := 8;
pnt.y := 6;
hChild := ChildWindowFromPoint(Result, pnt);
if hChild <> Zero then
begin
GetWindowRect(hChild, eRect);
pnt.x := 8;
pnt.y := (eRect.Bottom - eRect.Top)+6;
hChild := ChildWindowFromPoint(Result, pnt);
if hChild <> Zero then
begin
if GetWindowRect(hChild, LBRect) then
MoveWindow(Result, Left, Top, Width,(LBRect.bottom - eRect.Top) +3, False);
end;
end;
end;}
{I have a method to use One string (ListItems), as a #0 delimited string to
add all of the Items to this combo box at once. Just to show you, I have
placed the #255 charater, at the begining of the string and test for it.
I set a PChar variable pGet to the memory address of the second charcter
in the ListItems string}
if (ListItems <> '') and (ListItems[1] = #255) then
begin
pGet := @ListItems[2];
Lim := Zero; {LIM is just a "Safety" test in-case you forget to put an extra
#0 at the end of your ListItems string}
while Lim < 256 do
begin
Inc(Lim);
SendMessage(Result,CB_ADDSTRING, Zero, Integer(pGet));
{the CB_ADDSTRING message will only read the ListItems string until the next #0}
pGet := StrEnd(pGet);
{reset the pGet pointer to the next charater after the #0 ,
with StrEnd and Inc(pGet)}
Inc(pGet);
if pGet^ = #0 then Break;
{you will need to have a #0 at the end of your ListItems string,
so this while loop will end, see the constant CB2Items to see how to
construct a ListItems string}
end;
SendMessage(Result, CB_SETCURSEL, Zero, Zero);
end;
end;
end.
|
|
You could use the MakeComboBox( ) function as a "One Size Fits All" function, but the rest of this unit has not been developed to use as Universal Combo Box code, but this unit will keep the combo box code together.
You can create your own units and then try to make some code that is reusable. |
![]()
Next
The next lesson shows you how to have your programs use the XP Theme.
12. Xp Themes Manifest

H O M E 