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. If you look at some of the previous program's code here, like 7-Button and Edit controls you will see that I did not create any other Units for code containers, and the code in that program was getting long. I did not want to code outside of the program file, to show you how you would code only using the DPR. But creating units as code containers, can really help you to organize your code and you may be able to create a unit or two that can do some of the common tasks (font and window creation) that you can use in many of your projects. 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. Owner Drawn Combo Box 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.
Code for InUnitsU.pas 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