CS-360 Fall, 2000 Class 2 R. Eckert MICROSOFT WINDOWS WIN32 API PROGRAMMING -- AN EXAMPLE OF EVENT-DRIVEN, GRAPHICS-ORIENTED PROGRAMMING EXAMPLE--The user moves the mouse cursor over a program's window area and clicks the left mouse button. Windows decodes hardware signals from the mouse. Figures out which window the user has selected. Sends a message to its program: "User has clicked over (X,Y)" "Do something and return control to me" Program should: Read message data, Do what's needed Return control to Windows The following figure illustrates how Windows responds to events and interacts with the various applications that are running.OVERVIEW OF THE STRUCTURE OF A WINDOWS PROGRAM (2 main tasks)- 1. Initial activities 2. Process messages from Windows (the message loop) PSEUDO CODE-- Initialize variables, memory space Create and show the program's window Loop Fetch any message sent from Windows to this program If message is QUIT Terminate program, return control to Windows If message is something else Do appropriate actions based on the message ID and parameters Return control to Windows End Loop ESSENTIAL PARTS OF A WINDOWS PROGRAM-- I. THE SOURCE PROGRAM (.C or .CPP FILE): A. The WinMain() function-- 0. Include files, variable/function declarations, initialization, etc. 1. Register the class from which the program window will be derived. 2. Create a window based on a registered class. 3. Show the window and cause it to update (repaint) its client area. 4. The Message Loop--Get messages from Windows and dispatch back to Windows for forwarding to the correct callback message-processing function. Messages are usually the result of user actions. B. The WndProc() function--A callback function that acts on messages from Windows. It's a callback because Windows calls it. II. THE RESOURCE SCRIPT (.RC FILE): Resource data--Windows static data. Separate from code and dynamic data. Compiled by a separate "Resource Compiler". Examples: Keyboard Accelerators, Bitmaps, Cursors, Dialog Box Definitions, Fonts, Icons, Menus, String Tables. Separation of static resources and program code means: Reduced memory demands. Separation of programmer and designer tasks. Changes can be made to the user interface without touching the code. NOW WE LOOK AT THE DETAILS (Refer to the examples WINAPP1 and WINAPP2)-- I. THE SOURCE PROGRAM (.CPP FILE) #include <windows.h> // Windows header file - always included // Contains constant and function definitions Declare the Window Procedure-- LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM); A. THE WinMain() FUNCTION-- int PASCAL WinMain (HANDLE hInstance, HANDLE hPrevInstance, LPSTR lpszCmdLine, int nCmdShow); [Under Visual C++ 6.0, the first two parameters must be of type HINSTANCE.] Every Windows program must have a WinMain()-- -Like main(), it starts first. -It returns an integer exit code to Windows which does nothing with it. -PASCAL means left-to-right parameter passing on stack, which is faster than the C convention. Four parameters are passed from Windows to WinMain(): -hinstance: Instance handle--an ID # created by Windows when application starts. Each instance has a unique handle which identifies the application's data. [Handle--A pointer to a pointer. Handles locate objects in memory even though Windows moves things around.] -hPrevInstance: If another copy of the program is started, this will contain the hInstance value for the last copy started; it is NULL (0) if no other copy is running. (Under Win32 this is not used.) -lpszCmdLine: A pointer to a character string containing the command line arguments passed to the program. (like argc, argv in a DOS C program). -nCmdShow: An integer passed to the program's ShowWindow() function. Windows is telling the application whether its window is to appear minimized, as an icon, normal, or maximized when it's first displayed. Hungarian Notation--Used very commonly in Windows programs: -Helps to clarify types of variables. -Precede a variable name with key letters representing its type. -Named after Hungarian Microsoft programmer, Charles Simonyi. prefix data type --------------------------------------------------- by BYTE (unsigned char) b BOOL (int, use only TRUE and False, values: 1 and 0) c char dw DWORD (double word, 4-byte unsigned long int) fn function h handle. An ID value Windows uses internally to keep track of memory blocks, window ID values, etc. i int (two byte signed) l long (4 bytes) n short (int) near pointer p pointer s character string sz null-terminated character string w word (two bytes) These can be combined -- e.g., lpsz: long pointer to a null-terminated string. 1. REGISTERING THE WINDOW CLASS (New Windows Classes) New windows are created from "classes". All windows of a given class have the same properties: background color, cursor shape, pass Windows messages to the same function. We can use predefined classes (e.g. "BUTTON") or define and register a new class. There can be many instances based on the same class. Windows can only be created if their class has been "registered"-- RegisterClass()--Takes one parameter--a pointer to a WNDCLASS structure. The program must fill in the members of the class describing the window class before calling RegisterClass(). typedef struct tagWNDCLASS { UINT style; //bitwise or'd binary flags specify window style LRESULT CALLBACK lpfnWndProc)(); // pointer to message-processing function int cbClsExtra; //count of extra bytes after window class, usually 0 int cbWndExtra; //count of extra bytes after window instance //both could be used to reserve space for extra data HINSTANCE hInstance; //creating program's instance handle HICON hIcon; //handle to the window class icon HCURSOR hCursor; //handle to the window class cursor shape HBRUSH hBackground; //window class background brush LPSTR lpszMenuName //pointer to resource name of class menu LPSTR lpszClassName; //pointer to name of the window's class } WNDCLASS; To register the class of our window, we must set up the members of a WNDCLASS variable and call RegisterClass() as in the following sample code: WNDCLASS wndclass; wndclass.style = CS_HREDRAW | CS_VREDRAW ; wndclass.lpfnWndProc = WndProc ; wndclass.cbClsExtra = 0 ; wndclass.cbWndExtra = 0 ; wndclass.hInstance = hInstance ; wndclass.hIcon = LoadIcon (hInstance, "MYICON") ; wndclass.hCursor = LoadCursor (NULL, IDC_ARROW) ; wndclass.hbrBackground = (HBRUSH)GetStockObject (WHITE_BRUSH) ; wndclass.lpszMenuName = "MYMENU" ; wndclass.lpszClassName = "MyClass" ; if (!RegisterClass (&wndclass)) return 0 ; If the call to RegisterClass(&wndclass) is successful, a non-zero result will be returned. If not, we should return a 0 to Windows, which causes execution to terminate. The WNDCLASS fields for WINAPP1.CPP window are: .style-- CS_HREDRAW|CS_VREDRAW--bitwise or operator. It specifies that windows of this class should be redrawn if either the horizontal or vertical size changes. CS_HREDRAW = 00000010B CS_VREDRAW = 00000001B or'd value = 00000011B These are examples of binary flags--each bit means something; the bitwise or operation is a way to tell Windows that several conditions are to be set. It's a way to specify combinations. (See "Help | Search | WNDCLASS"). .lpfnWndProc--WndProc: the name (address) of the function to be called when windows sends messages. It could have any name you like, but that name must be registered. Here the name used is WndProc. This function must be declared--usually at the top of the source file or in a header file included at the top of the source file. .cbClsExtra & .cbWndExtra--Here no extra memory is allocated for this class. .hInstance--Specifies which instance the WinProc() lies in. It's this instance (passed as a parameter by Windows to WinMain()). It's used as a unique identification for the application calling RegisterClass(). .hIcon, .hCursor--Use Windows load functions to use some predefined objects. IDI_APPLICATION--A constant meaning the standard windows icon. IDC_ARROW--A constant meaning the standard arrow pointer. (These are defined in the windows.h file) Examples: LoadIcon (NULL, IDI_APPLICATION); LoadCursor (NULL, IDC_ARROW); .hbrBackground--Use Windows GetStockObject() here to obtain a solid white brush which paints the background color of the window. This is a handle to a brush -- a graphics term referring to the pattern of pixels used to fill an area. A brush is an example of a Windows "Graphics Device Interface" (GDI) object. Many other GDI objects are used to "paint" on the client area of a window, as we'll see later. Under Visual C++ 6.0 the handle to the brush obtained by the call to GetStockObject() must be typecast to HBRUSH. in other words, the line of code that sets up the hbrBackground member of the WNDCLASS structure would have to be: wndclass.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH); .lpszMenuName=NULL: A menu can be described in the resource script file. (See WINAPP2.RC). In WINAPP1, there is no menu, so this is set to null. .lpszClassName--The name given to this window class; This will be used when we create a window based on the class. After registering the new window class, the program must create the window. Any number of windows based on this class can be created. 2. CREATING THE PROGRAM WINDOW CreateWindow() creates the program's window & returns a handle (a unique ID) value of type HWND (for the window created). Arguments to CreateWindow()-- window class name window caption window style (Boolean OR of different predefined style masks) initial x position in pixels initial y position initial width initial height parent window handle (if main window, no parent ==> NULL) window menu handle (NULL if no menu or if class menu is to be used) program instance handle (passed in from Windows) creation parameters (can be used to access extra data, usually NULL) The WINAPP1.CPP program creates its window as follows: HWND hWnd ; hWnd = CreateWindow ("MyClass", "First Windows App", WS_OVERLAPPEDWINDOW, 100, 50, 400, 200, NULL, NULL, hInstance, NULL); 3. SHOW THE WINDOW AND CAUSE ITS CLIENT AREA tO BE UPDATED SHOW THE WINDOW: ShowWindow (hWnd,nCmdShow); -- Makes the window just created visible on the screen. -hWnd: Specifies which window to make visible. -nCmdShow: Specifies how (normal, minimized, etc.) -Set by the Windows environment when the program is started -Value is passed in from Windows -"Normal" can be overridden by starting the program from the program manager "File | Run" and select "Run minimized" CAUSE THE WINDOW'S CLIENT AREA TO BE UPDATED (PAINTED WITH THE BACKGROUND BRUSH): UpdateWindow (hWnd); 4. THE MESSAGE LOOP Any time the user interacts with the window, the Windows operating system sends a message to the window's WndProc(). The interaction could be mouse movement, a mouse button click, a keyboard key press, a resizing or moving operation that causes some area to be exposed (with a resultant need to be repainted), or any number of other operations. (Windows sends many other messages as well.) A message consists of some data placed in a block of memory: A C or C++ structure-- typedef struct tagMSG { HWND hwnd; /* handle of target window */ UINT message; /* message ID value--always a WM_* constant */ WPARAM wParam; /* a double word of data passed in the message */ LPARAM lParam; /* a second double word of data in the message */ DWORD time; /* time in msec message was sent */ POINT pt; /* mouse cursor (x,y) position when sent */ } MSG; where: typedef struct tagPoint { int nx; int ny; } POINT; Windows fills in all this data before message is sent. A Windows program must continually check for incoming messages. This is done by using a small program loop called the message loop. One possibility: Use GetMessage() GetMessage() function-- Reads the next message from the application's message queue and fills the fields of a variable of type MSG. We want put our call to GetMessage() in a loop that continually retrieves the message: while (GetMessage(&msg, NULL, 0, 0)) { ... } -If Windows has sent the program a message, GetMessage() puts all the message data into the structure pointed to by msg. BOOL GetMessage (lpMsg, hWnd, uFirstMsg, uLastMsg); lpMsg--Address of the structure where Windows should put the message data. hWnd--Handle of the window from which the message was sent (NULL ==> receive messages from all windows belonging to the application that made the call to GetMessage()). Last 2 parameters--Specify a range of message values to be retrieved (0 means all possible messages). If there are no messages, Windows retains control of the system and does other stuff (with other programs) until a message to the program is generated. Only then does GetMessage() return to your program. This is the key to cooperative multitasking! For all messages other than WM_QUIT (generated by the WndProc(), as we'll see, after user actions like a double click on System menu button or <Alt>-<F4> keyboard combination), TRUE (non-zero) is returned to the application's message loop and the while loop continues. GetMessage() returns 0 if the message is a WM_QUIT. This causes the while loop to exit. Then the return(msg.wParam) returns control to Windows, effectively terminating the application. All well-behaved Windows applications must have a message loop! GetMessage() is one of the few functions that returns control to Windows, thus letting other applications run. Under Windows 3.xx cooperative multitasking, if a program never yields control to Windows, no other programs are allowed to run--which is very bad! Under Windows 95 and NT, this is not true, since there is pre-emptive multitasking. Even so, it is not a good idea to let your program be a "hog." The following figure shows how the main message loop, implemented with GetMessage(), works.
MESSAGE PROCESSING--Typically the main message loop is implemented as follows (This is done in the WINAPP1.CPP program): while (GetMessage(&msg, NULL, 0, 0)) /* message loop */ { TranslateMessage(&msg); /* translate keyboard messages */ DispatchMessage(&msg); /* send message to WndProc() */ } return (msg.wParam); TranslateMessage (&msg)-- This function "cooks" keyboard input and converts raw key codes to ANSI codes that can be used by the program. Actually it converts WM_KEYDOWN messages (sent when any key goes down) to WM_CHAR messages that recognize specific keys and key combinations, e.g., <Shift>-<anything>. These WM_CHAR messages are processed in a well-defined way by the default Window procedure. This provides a keyboard alternative to using the mouse to select things. Most Windows programs use TranslateMessage() in their message loops. DispatchMessage (&msg)-- Since message processing will be done in the function WndProc(), the program must tell Windows to pass any message data to that function. This is done with DispatchMessage(&msg), which sends the message on to Windows. Windows, in turn, forwards it to the function specified in the lpfnWndProc member of the WNDCLASS structure--the WndProc(). Data is sent along in the message structure that was filled in by GetMessage(). This happens each time a message is received. Messages are continuously retrieved from the program's queue and dispatched to its WndProc(). B. THE WINDOW PROCEDURE-- This is a "callback" function (named so because it is called by Windows). It contains a big switch/case statement that looks at the message ID of the current message and acts appropriately on any "interesting" messages. LRESULT CALLBACK WndProc (HWND hWnd, UINT wMessage, WPARAM wParam, LPARAM lParam) Parameters--The same as the first four fields of the MSG structure-- -the window the message is associated with. -the message ID--what the message is. -any data associated with the message (wParam & lParam message parameters): this data is specific for each type of message. wParam--"word" parameter; (16 bits under Win16). lParam--long parameter (32 bits). [This changed under Win32 -- Now both are 32 bits.] The following figure shows how Windows and the application interact in the processing of messages.
The WndProc() in the WINAPP1.CPP example looks for the following message: WM_DESTROY--Generated when the user double clicks on the system menu, chooses "Close" from the system menu, or presses <ALT>-<F4>. This message means that Windows is in the process of removing the window from the screen. Any other messages are handled by DefWindowProc(), the default Window procedure. Other important messages are: WM_COMMAND--User interacted with a menu item, wParam=Menu item ID). WM_LBUTTONDOWN--User pressed the left mouse button (lParam contains the x,y coordinates). WM_RBUTTONDOWN--User pressed the right mouse button. WM_CHAR--User pressed a key or key combination that corresponds to a valid ANSI code (wParam contains the ANSI code). It is essential that our programs trap the WM_DESTROY message. If not the GetMessage() loop will carry on forever, even though the window is gone! WINAPP1's Response to WM_DESTROY-- -The program calls PostQuitMessage(0). -PostQuitMessage() causes Windows to send a WM_QUIT message to the program's message queue. Recall that when this is encountered, GetMessage() returns a 0 (the only message for which it does that). -This causes the program to exit WinMain()'s while loop. -It then hits the return(msg.wParam) instruction. -The value returned is that accompanying the WM_QUIT message (0), and control goes to Windows (doesn't use the 0), which releases all memory used by the program. The following figure illustrates how the WM_DESTROY message should be handled.
If it's some other message, the program calls DefWindowProc()--the default procedure. This preforms default actions for the message received; e.g., pressing the left mouse button while moving the cursor is over title bar will result in default dragging action. (Movement, repainting are all done by Windows default function.) The DefWindowProc() function is in Windows. Our WINAPP1.CPP program code overrides default window procedure action for the WM_DESTROY message. USING MICROSOFT DEVELOPER STUDIO 97 TO CREATE A Windows Win32 VISUAL C++ APPLICATION As you will see, the process of creating a Win32 API application is very similar to creating a Windows Console application, discussed earlier. Detailed steps are given below. Startup: 1. From Windows (NT or 95) click 'Start' on Task Bar 2. Select 'Programs | Microsoft Visual C++ | Microsoft Visual C++5.0' 3. Close the "Tip of the Day" box (if it comes up) You should see the window containing three subwindows: left: the Project Workspace Window right: the Editor Window bottom: the Output Window Creating the Project: 1. Select 'File | New' 2. Click on 'Projects' Tab (if not chosen already) 3. Select 'Win32 Application' 4. Name the project (e.g. winapp1) The system will use the directory stated in 'Location' as the parent directory for the project directory that it will create. You can change this parent directory as you like. Be sure to use a hard disk drive. The project directory will have the name you give in this step, and all files created by Developer Studio will be in that directory. 5. Click 'OK' Note there are now three tabs in the project workspace window: ClassView -- to look at the classes in a C++ program FileView -- to look at the various files in the project InfoView -- to access on-line help [Under Visual C++ 6.0, there is an additional step after step 3. After you choose 'Win32 Application', a dialog box comes up that asks you what kind of windows application you would like to create: An Empty Project, A Simple Win32 application, or A Typical "Hello World: application. For the kinds of applications we'll be creating in this class, select the first choice (An Empty Project), and then click the "Finish" button.] Adding a new C++ source file to the project: 1. Select 'File | New' and make sure that the 'Files' tab is selected. 2. Choose 'C++ Source File', enter the name you want to give the file (without extension) in the 'File name' edit control, and click the 'OK' button. This will cause an empty document to appear in the Editor Window. In addition, if you now select the 'FileView' tab in the Project Workspace Window and expand the workspace files listing, you will see that a .cpp file has been added to the project. 3. Type in the source code. Or easier yet, copy and paste source code from the example programs given on the course web pages and edit it to do whatever it is that your application is supposed to do. [Inserting source files that have already been created into the project: 1. Use any editor to create the .cpp (or .c), [and also any .h, .rc files used]. Save them in the directory that contains your other project files-- i.e., the directory that was created above by Developer Studio. If the source files already exist, just copy them into the project's directory. 2. From the menu select 'Project | Add to Project | Files' 3. Make sure the 'Look In' box contains the directory with your files. The files should appear in the larger box below. 4. Select the .c or .cpp file(s) (e.g., winapp1.cpp) and click 'OK'. If you now click on the FileView tab in the Workspace Window, you will see that the file has been included in the project. To see it in the Editor Window, click on the file name from the FileView window.] Building the Project: 1. Select 'Build | Build project_name.exe' (e.g., Build example.exe) The Project will be compiled/linked. Messages (errors) will appear in the Output Window. (A shortcut keystoke that will do the same thing is F7.) Running the Application: Select 'Build | Execute project_name.exe' (e.g., Execute example.exe) The application will execute. (A keyboard shortcut is <Ctrl><F5>.) (Or, if an red exclamation point (!) icon appears on a toolbar, click it.) Cleanup: The entire project (LOTS of files--over 3 Megabytes worth!) will be in the directory you chose when you created the project on C drive. The most important files in this directory are the .dsp (Designer Studio Project) and .dsw (Designer Studio Workspace) files. You should copy them (along with all your source files) to your diskette. In future sessions, the project can be brought into Designer Studio by copying the .dsp, .dsw, and source files to the C drive (in a temporary directory) and selecting 'File | OpenWorkspace'. Open the temporary directory, select 'Files of Type:' 'Project' from the lower list box, and double click on your .dsp or .dsw file in the upper box. You can then rebuild the project. After building a project, the project directory will will have a DEBUG subdirectory. The executable (.exe) will be in that DEBUG subdirectory. You should also copy it to your diskette. After you are sure that you have all your source files, your .dsp and .dsw files, and your .exe file on your diskette, delete the project directory from the hard drive to clean it up. Exiting Developer Studio: Select 'File | Exit'.