In this portion of the chapter, we're going to use the API application template developed at the beginning of the chapter to create an API application that allows the user to view icons stored in executable files and dynamic link libraries. While creating this application, you'll learn how to do the following:
Create resource files
Specify a custom icon for the executable file
Create a main menu
Create child controls
Use common dialog boxes
Properly draw to the window in response to the WM_PAINT message
Extract icons from other executables and dynamic link libraries
Use an external manifest to enable child controls to use XP visual styles
Before we start creating the application, you should take a look at the final product. It is displayed in Figure 23-3.
Figure 23-3: The Icon Viewer
It's actually easy to create a menu in an API application. You only have to do these three simple things:
Create a resource file that contains the menu.
Link the aforementioned resource into the executable file.
Display the menu on the main window by assigning the menu's name to the lpszMenuName field of the WNDCLASS record.
The resource file that contains the menu (or other resources) can be created in any text editor, as long as you save the resource file with the .rc extension. The resource file that contains the main menu of the Icon Viewer is called Menu.rc and its contents are displayed in Listing 23-3.
Listing 23-3: The Menu.rc resource file
Main_Menu MENU { POPUP "&File" { MENUITEM "&Open...", 101 MENUITEM SEPARATOR MENUITEM "E&xit", 102 } }
It is clearly visible that the statements in the Menu.rc file define a simple File menu that has two commands (Open and Exit) and a separator between them. The numbers 101 and 102 at the end of the MENUITEM statements are ID numbers, which are used in the window procedure to identify the selected menu item.
Now that you've created the resource file, you have to compile it into a .res file so that it can be linked into the executable file. To compile the .rc file, you have to use the Resource Compiler (brcc32.exe). To avoid typing the command to compile the resource file over and over again, you should create a batch file to perform the compilation of the resource. Here are the contents of the CompileMenu.bat file that can be used to compile the Menu.rc resource file:
brcc32 Menu.rc
To use the menu from the compiled Menu.res file, you have to include the Menu.res file into the executable using the $R compiler directive and you have to assign the menu's name to the lpszMenuName field of the WNDCLASS:
{$R Menu.res} wc.lpszMenuName := 'Main_Menu'; { load the menu from the resource }
If you've done everything correctly and recompiled the application, you should be able to see the File menu on your main window.
Figure 23-4: The main menu loaded from the resource file
Buttons, like all other windows, can be created with the CreateWindow function. Actually, there are several predefined classes that can be created with the CreateWindow function. These are: BUTTON, COMBOBOX, EDIT, LIST- BOX, RICHEDIT_CLASS, SCROLLBAR, and STATIC (label).
When you're creating buttons (or other windows), you have to declare an HWND variable that will hold the button's handle and preferably declare a constant with the unique ID value for the button. To create a standard button, you have to call the CreateWindow function and pass the following parameters:
Pass "BUTTON" as the lpClassName parameter.
Pass a WS_CHILD, WS_VISIBLE, BS_PUSHBUTTON, or BS_TEXT combination as the dwStyle parameter to create a child window that is initially visible, looks like a standard button, and displays text on its surface.
Pass the MainWnd variable as the hWndParent parameter to create the button as a child window of the main window.
Pass the unique ID value of the button as the hMenu parameter.
The following listing shows how to create the Previous and Next buttons shown in Figure 23-3.
Listing 23-4: Creating the Previous and Next buttons
const PREV_BUTTON = 103; NEXT_BUTTON = 104; var prevButton: HWND; { handle of the Previous button } nextButton: HWND; { handle of the Next button } ... prevButton := CreateWindow('BUTTON', 'Previous', WS_CHILD or WS_VISIBLE or BS_PUSHBUTTON or BS_TEXT, 10, 115, 75, 25, MainWnd, PREV_BUTTON, HInstance, nil); nextButton := CreateWindow('BUTTON', 'Next', WS_CHILD or WS_VISIBLE or BS_PUSHBUTTON or BS_TEXT, 160, 115, 75, 25, MainWnd, NEXT_BUTTON, HInstance, nil);
To implement the functionality of the File menu commands and the two buttons, we have to handle the WM_COMMAND message in the application's window procedure. The control that sends the WM_COMMAND message is specified in the wParam parameter of the message. Thus, to close the application when the user selects File ® Exit, you have to write something like this:
const FILE_EXIT = 102; {this must match the menu item ID specified in the .rc file } ... function WindowProc(Wnd: HWND; Msg, WParam, LParam: LongInt): LongInt; stdcall; begin Result := 0; case Msg of WM_COMMAND: begin case WParam of FILE_EXIT: SendMessage(MainWnd, WM_CLOSE, 0, 0); end; // case WParam end; // WM_COMMAND end; // case end;
To enable the user to extract icons from an EXE file or a DLL, we first have to display the Open dialog box to enable the user to select a file. The API way to display an Open dialog box is somewhat harder than using the TOpenDialog component. To display the Open dialog box in an API application, you have to do the following:
Add the CommDlg unit to the application's uses list to access the necessary records and functions.
Fill an OPENFILENAME record with the appropriate data.
Call the GetOpenFileName function to display the Open dialog box and retrieve the file name.
The following listing contains a function that displays the Open dialog box and returns the selected file name if the user selects OK or an empty string if the user selects Cancel.
Listing 23-5: Displaying the open dialog box
function OpenFileDialog: string; var ofn: OPENFILENAME; fileName: array[0..MAX_PATH] of Char; const FILTER = 'Executable Files' + #0 + '*.exe' + #0 + 'Dynamic Link Libraries' + #0 + '*.dll' + #0#0; begin fileName := ''; ZeroMemory(@ofn, SizeOf(OPENFILENAME)); ofn.lStructSize := SizeOf(OPENFILENAME); ofn.hWndOwner := MainWnd; ofn.lpstrFilter := FILTER; ofn.lpstrFile := fileName; ofn.nMaxFile := MAX_PATH; GetOpenFileName(ofn); Result := string(fileName); end;
The OPENFILENAME record is pretty big and has a lot of fields that are not needed in this application. The ZeroMemory function is used here to set these unused fields to 0.
When displaying the Open dialog box you must specify a valid buffer in the lpstrFile parameter that will receive the file name and properly set the hWndOwner field. To hide unneeded items in the dialog box, you should also specify a valid filter string.
If you look at the local FILTER constant, you'll notice that the filter string in an API application differs from the filter string used by the VCL components. Each part of the filter has to be separated by a null character, not the vertical bar character. Also, the filter string in an API application must be terminated by two null characters.
The above code will only be able to display the Open dialog on NT 5.0 (Windows 2000) and later operating system versions because the OPENFILENAME record in the CommDlg unit contains three NT 5.0-specific fields:
pvReserved: Pointer; dwReserved: DWORD; FlagsEx: DWORD;
If you want to target Windows 98 as well, you cannot pass SizeOf(OPENFILENAME) to the lStructSize field. To have the Open dialog work on Windows 98, discard the size of these three fields from the lStructSize field:
ofn.lStructSize := SizeOf(OPENFILENAME) - SizeOf(ofn.pvReserved) SizeOf(ofn.dwReserved) - SizeOf(ofn.FlagsEx);
The easiest way to extract icons from an executable file is to use the ExtractIcon function:
function ExtractIcon(hInst: HINST; lpszExeFileName: PChar; nIconIndex: UINT): HICON; stdcall;
The ExtractIcon function returns a handle to the icon, which must be released with a call to DestroyIcon. So, to prevent the possibility of a memory leak, we should create a function that automatically releases the extracted icon before extracting a new one. Since we're going to draw the extracted icon, the function should also refresh the window when a new icon is extracted. The window can be refreshed with a call to the InvalidateRect function.
The following listing displays the entire function that extracts icons from an executable file. Note that the fileName and the icon variables used in the function are global variables. The icon variable contains the handle of the extracted icon, and the fileName variable contains the path and file name of the selected executable.
Listing 23-6: Extracting icons
procedure DisplayIcon(IconIndex: Integer); begin if icon <> 0 then DestroyIcon(icon); { remove the old icon from memory } if fileName <> '' then icon := ExtractIcon(HInstance, PChar(fileName), IconIndex); { refresh the window to display the newly extracted icon } InvalidateRect(MainWnd, nil, True); end;
Now that you've created the necessary utility functions, you can implement the File ® Open command, as shown in Listing 23-7.
Listing 23-7: The File Ø Open command
FILE_OPEN: begin fileName := OpenFileDialog; { select a new EXE or DLL } if fileName <> '' then { if the user clicked OK } begin { pass -1 as the last parameter to get the icon count } iconCount := ExtractIcon(HInstance, PChar(fileName), Cardinal(-1)); { if there are icons in the EXE, display the first one } if iconCount > 0 then begin currIcon := 0; DisplayIcon(currIcon); end else begin DisplayIcon(0); { call this to remove the old icon } MessageBox(MainWnd, 'The application or library ' + 'contains no icons!', 'Message', MB_OK or MB_ICONINFORMATION); end; // if iconCount end; // if fileName end; // FILE_OPEN
The only thing left to do now is handle the WM_PAINT message and draw the extracted icon on the window.
The WM_PAINT message is sent to the window when all or part of the window should be repainted. If you want to draw in response to the WM_PAINT message, you should not use the GetDC and ReleaseDC functions but rather the BeginPaint and EndPaint functions:
function BeginPaint(hWnd: HWND; var lpPaint: TPaintStruct): HDC; stdcall; function EndPaint(hWnd: HWND; const lpPaint: TPaintStruct): BOOL; stdcall;
The BeginPaint function prepares the window for drawing and returns a device context handle. When you call BeginPaint to begin drawing on a window, you must call EndPaint to finish drawing.
The following listing shows the WM_PAINT message handler of the Icon Viewer application.
Listing 23-8: Drawing icons and text in response to the WM_PAINT message
var selMessage: string; dc: HDC; ps: PAINTSTRUCT; ... WM_PAINT: begin dc := BeginPaint(MainWnd, ps); if icon <> 0 then { if we have a valid icon } begin SetBkMode(dc, TRANSPARENT); { select the default system font into the device context } SelectObject(dc, GetStockObject(DEFAULT_GUI_FONT)); selMessage := Format('Icon %d of %d', [Succ(currIcon), iconCount]); { display the message using the previously selected system font } TextOut(dc, 10, 10, PChar(selMessage), Length(selMessage)); { display the icon } DrawIcon(dc, 10, 40, icon); end; EndPaint(MainWnd, ps); end; // WM_PAINT
To fully understand how the Icon Viewer application works and where the previously described pieces of code go, you should take a closer look at Listing 23-9 since it contains the fully commented source code of the application.
Listing 23-9: The Icon Viewer
program Generic; uses Windows, Messages, SysUtils, CommDlg, ShellAPI; {$R Menu.res} const MY_CLASS = 'DelphiAPIWindow'; APP_CAPTION = 'Icon Viewer'; FILE_OPEN = 101; FILE_EXIT = 102; PREV_BUTTON = 103; NEXT_BUTTON = 104; var Msg: TMsg; MainWnd: HWND; { the main window's handle } wc: WNDCLASS; { the window class } prevButton: HWND; { handle of the Previous button } nextButton: HWND; { handle of the Next button } icon: HIcon; { the selected icon's handle } iconCount: Integer; { the number of icons in an EXE } currIcon: Integer; { the currently selected item } fileName: string; { the file name of the EXE we're viewing } screenWidth: Integer; screenHeight: Integer; function OpenFileDialog: string; var ofn: OPENFILENAME; fileName: array[0..MAX_PATH] of Char; const FILTER = 'Executable Files' + #0 + '*.exe' + #0 + 'Dynamic Link Libraries' + #0 + '*.dll' + #0#0; begin fileName := ''; ZeroMemory(@ofn, SizeOf(OPENFILENAME)); ofn.lStructSize := SizeOf(OPENFILENAME); ofn.hWndOwner := MainWnd; ofn.lpstrFilter := FILTER; ofn.lpstrFile := fileName; ofn.nMaxFile := MAX_PATH; GetOpenFileName(ofn); Result := string(fileName); end; procedure DisplayIcon(IconIndex: Integer); begin if icon <> 0 then DestroyIcon(icon); { remove the old icon from memory } if fileName <> '' then icon := ExtractIcon(HInstance, PChar(fileName), IconIndex); { refresh the window to display the newly extracted icon } InvalidateRect(MainWnd, nil, True); end; function WindowProc(Wnd: HWND; Msg, WParam, LParam: LongInt): LongInt; stdcall; var selMessage: string; dc: HDC; ps: PAINTSTRUCT; begin Result := 0; case Msg of WM_COMMAND: begin case WParam of FILE_EXIT: SendMessage(MainWnd, WM_CLOSE, 0, 0); FILE_OPEN: begin fileName := OpenFileDialog; { select a new EXE or DLL } if fileName <> '' then { if the user clicked OK } begin { pass -1 as the last parameter to get the icon count } iconCount := ExtractIcon(HInstance, PChar(fileName), Cardinal(-1)); { if there are icons in the EXE, display the first one } if iconCount > 0 then begin currIcon := 0; DisplayIcon(currIcon); end else begin { call DisplayIcon to remove the old icon and to refresh the screen } DisplayIcon(0); MessageBox(MainWnd, 'The application or library ' + 'contains no icons!', 'Message', MB_OK or MB_ICONINFORMATION); end; // if iconCount end; // if fileName end; // FILE_OPEN NEXT_BUTTON: begin if currIcon < Pred(iconCount) then begin Inc(currIcon); DisplayIcon(currIcon); end; end; // NEXT_BUTTON PREV_BUTTON: begin if currIcon > 0 then begin Dec(currIcon); DisplayIcon(currIcon); end; end; // PREV_BUTTON end; // case WParam end; // WM_COMMAND WM_PAINT: begin dc := BeginPaint(MainWnd, ps); if icon <> 0 then { if we have a valid icon } begin SetBkMode(dc, TRANSPARENT); { select the default system font into the device context } SelectObject(dc, GetStockObject(DEFAULT_GUI_FONT)); selMessage := Format('Icon %d of %d', [Succ(currIcon), iconCount]); { display the message using the previously selected system font } TextOut(dc, 10, 10, PChar(selMessage), Length(selMessage)); { display the icon } DrawIcon(dc, 10, 40, icon); end; EndPaint(MainWnd, ps); end; // WM_PAINT WM_CLOSE: begin if icon <> 0 then DestroyIcon(icon); { release the icon } DestroyWindow(prevButton); { destroy the Prev button } DestroyWindow(nextButton); { destroy the Next button } end; // WM_CLOSE WM_DESTROY: PostQuitMessage(0); { terminate the app } else Result := DefWindowProc(Wnd, Msg, WParam, LParam); end; // case end; begin wc.style := CS_DROPSHADOW; wc.lpfnWndProc := @WindowProc; wc.cbClsExtra := 0; wc.cbWndExtra := 0; wc.hInstance := HInstance; wc.hCursor := LoadCursor(0, IDC_ARROW); wc.hbrBackground := COLOR_BTNFACE + 1; wc.lpszMenuName := 'Main_Menu'; { load the menu from the resource } wc.lpszClassName := MY_CLASS; if RegisterClass(wc) = 0 then Exit; { create the window and emulate the poDesktopCenter form position } screenWidth := GetSystemMetrics(SM_CXSCREEN); screenHeight := GetSystemMetrics(SM_CYSCREEN); MainWnd := CreateWindow(MY_CLASS, APP_CAPTION, WS_BORDER or WS_SYSMENU, screenWidth div 2 - 125, screenHeight div 2 - 100, 250, 200, 0, 0, HInstance, nil); prevButton := CreateWindow('BUTTON', 'Previous', WS_CHILD or WS_VISIBLE or BS_PUSHBUTTON or BS_TEXT, 10, 115, 75, 25, MainWnd, PREV_BUTTON, HInstance, nil); nextButton := CreateWindow('BUTTON', 'Next', WS_CHILD or WS_VISIBLE or BS_PUSHBUTTON or BS_TEXT, 160, 115, 75, 25, MainWnd, NEXT_BUTTON, HInstance, nil); { use system font on the buttons } SendMessage(prevButton, WM_SETFONT, GetStockObject(DEFAULT_GUI_FONT), 0); SendMessage(nextButton, WM_SETFONT, GetStockObject(DEFAULT_GUI_FONT), 0); { display the main window } ShowWindow(MainWnd, SW_SHOWNORMAL); UpdateWindow(MainWnd); { message loop } while GetMessage(Msg, 0, 0, 0) do begin TranslateMessage(Msg); DispatchMessage(Msg); end; end.
Once you've finished the application, you should take care of two user interface details: replacing the default application icon with a custom one, and creating the external manifest file to enable the child controls to use XP visual styles.
To change the main icon of the application, open the Menu.rc file and write the following statement to define the MAINICON resource:
MAINICON ICON "Factory.ico"
The above statement uses the Factory icon that can be found in the X:\Pro- gram Files\Common Files\Borland Shared\Images\Icons directory.
After you've added the above statement to the Menu.rc resource file, you should recompile both the resource file and the application. The Delphi compiler will replace the default white application icon with your custom one, but the custom icon won't be displayed on the title bar of the main window. To display the icon on the main window's title bar, you have to use the LoadIcon function to load the icon from the resource file and assign the result of the LoadIcon function to the hIcon field of the WNDCLASS record:
wc.hIcon := LoadIcon(HInstance, 'MAINICON');
By passing the global HInstance variable as the first parameter in the LoadIcon function, you're telling the function that the requested resource is stored in the executable file.
The final step in the creation of the Icon Viewer is the manifest file that will render the child controls using the XP visual styles. You actually don't have to create the manifest because the sample manifest file already exists in the X:\Program Files\Borland\BDS\4.0\source\Win32\vcl directory under the name sample.manifest. You only have to copy it to the application directory and rename it as follows:
ApplicationName.exe.manifest
Since the Icon Viewer's executable is Generic.exe, you must rename the manifest file to Generic.exe.manifest, and you're done. The child controls will be rendered using XP visual styles.