CS-360 Fall, 2000 Class 14 R. Eckert MFC WINDOWS PROGRAMMING (APP/WINDOW APPROACH) The Microsoft Foundation Class (MFC) Library-- The Windows MFC Library consists of a hierarchy of C++ classes designed to facilitate Windows programming. To see a context sensitive map (hot-linked diagram) of the entire MFC class hierarchy, search the online help for "hierarchy chart".) Using the MFC library classes offers an alternative to using Win32 API functions. The following diagram illustrates how a Visual C++ Windows application can use either the Win32 API or MFC (or both) to write a program that tells the computer hardware what has to be done: There are about 200 MFC classes (versus 2000+ API functions). They provide a framework upon which to build Windows applications. They encapsulate most of the Win32 API in a set of logically organized classes. Some of the characteristics of MFC are-- 1. They offer the convenience of REUSABLE CODE, so that: -Many of the tasks common to all Windows applications are provided by MFC. -Our programs can inherit and modify this functionality as needed. -Therefore we don't need to recreate these tasks. -In other words, MFC handles many clerical details. 2. They produce smaller executables: -MFC programs are typically 1/3 the size of their API counterparts. 3. They can lead to faster program development-- -But there is a steep learning curve, especially for those who are not comfortable with the object-oriented programming paradigm. 4. MFC Programs must be written in C++ and require the use of classes, so the programmer needs a good grasp of: -How classes are declared, instantiated, and used -Encapsulation -Inheritance -Polymorphism--virtual functions THE MFC CLASS HIERARCHY (See online help on "Hierarchy Chart")-- At the top: CObject ("Mother of all classes"): provides run-time support features like serialization (disk reading/writing); all its functionality is inherited by any classes derived from it. Important derived classes-- CFile: Support for file operations. CDC: Encapsulates the device context (Graphical Drawing). CGdiObject: Base class for various drawing objects (bitmaps, pens, etc.) CMenu: Encapsulates menu management. CCmdTarget: Encapsulates process of message passing & is the parent of: CWnd: Base class from which all windows are derived; most common: CFrameWindow: ("normal" kind of window that can contain other windows--the kind that we've been using). CView: encapsulates process of displaying data. CWinThread: Defines a thread of execution. CWinApp: Most important class dealt with in MFC apps-- Encapsulates an MFC application. Controls the following aspects of Windows programs: Startup, initialization, execution, shutdown. An application should have one CWinApp object. When instantiated, the application begins to run. CDocument: Encapsulates the data associated with a program. The primary task in writing the code for an MFC program is to create classes. Most of these will be derived from one of the MFC library classes. MFC MEMBER FUNCTIONS-- Most of the functions an application will call are members of an MFC class. Examples: ShowWindow()--a member of CWnd class. TextOut()--a member of CDC. LoadBitmap()--a member of CBitmap. Applications can also call API functions directly, but it's more convenient to use MFC member functions. MFC GLOBAL FUNCTIONS-- MFC also defines important "global" functions that are not members of any MFC class. They always begin with Afx prefix (From Application FrameworKS). Example: AfxMessageBox()--since message boxes are predefined windows, it's possible to activate one independently from the rest of an application. Afx functions are either independent of or span the MFC class hierarchy. A MINIMAL MFC PROGRAM (APP/WINDOW APPROACH)-- The simplest MFC programs contain two classes we must derive from the hierarchy: An application class derived from CWinApp--defines the application. A window class usually derived from CFrameWnd--defines the application's main window. Every MFC program must have these two classes. MESSAGE PROCESSING UNDER MFC-- Just as in Win32 API programs, MFC programs must handle messages from Windows. In the API case the mechanism is a huge switch/case statement. MFC uses something called "message maps," which are lookup tables. Each table contains entries that consist of a message number and a pointer to a derived class member message-processing function. We must declare the message-processing functions and map them to the specific messages that our application is going to respond to. The mapping is done by "message-mapping macros." In an MFC application most windows use a window procedure supplied by the library. The message maps enable the library window procedure to find the class member function that corresponds to the current message. (The figures below attempt to illustrate the difference between how API and MFC applications respond to messages.) SPECIFIC STEPS IN WRITING A SIMPLE MFC PROGRAM (App/Window Approach)-- A. DECLARATIONS (.h) [See PROG1.H and MSG1.H for simple examples]: 1. Declare a window class derived from CFrameWnd (e.g., CMainWin); Class Members: The constructor Message-processing function declarations [e.g., afx_msg void OnChar()]. (There are none in PROG1.) DECLARE_MESSAGE_MAP() macro to: -allow windows based on this class to respond to messages; -declare that a message map will be used to map messages to functions; This should be the last class member declared. 2. Declare an application class derived from CWinApp (e.g., CApp): It Must override the CWinApp's virtual InitInstance() function. -This function is called each time a new instance of the application is started; i.e., when an object of this class is instantiated. B. IMPLEMENTATION (.CPP) [See PROG1.CPP & MSG1.CPP for simple examples]: 1. Define the constructor for the class derived from CFrameWnd (CMainWin)-- It should make a call to member function Create() to create the window. -This does what CreateWindow() does in API programming. 2. Define the message map for the class derived from CFrameWnd (CMainWin)-- BEGIN_MESSAGE_MAP(owner, base) [list of "message macros" [e.g., ON_WM_CHAR()] END_MESSAGE_MAP() (There are none in the PROG1 example.) 3. Define the message-processing functions declared in 1 above-- (There are none in the PROG1 example.) 4. Define the InitInstance() overriding function in the class derived from CWinApp (CApp) -- Should have initialization code for each new instance of the application: -Create a CMainWin object --> a pointer to program's main window. (This is used like hWnd in API programs to refer to the window.) -Invoke the object's ShowWindow() member function. -Invoke the object's UpdateWindow() member function. -Must return non-zero to indicate success. [MFC's implementation of WinMain() calls this function.] At this point the nature and form of the simple window and application have been defined, but neither exists until an application object derived from CWinApp (CApp) is instantiated. 5. Create an instance of the application class (CApp): This causes WinMain() to execute--it's now a part of MFC [WINMAIN.CPP]. WinMain() does the following: -Calls AfxWinInit(), which calls AfxRegisterClass() to register the window class. -Calls CApp::InitInstance() [the virtual function overridden in 2 above], which creates the window and shows it. -Calls CWinApp::Run(), which calls CWinThread::PumpMessage()-- which contains the GetMessage() loop. -After this returns (i.e., when the WM_QUIT message is received), AfxWinTerm() is called, which cleans up and exits. PROG1 APPLICATION--Just creates a skeleton frame window-- SPECIFIC STEPS IN CREATING AND BUILDING AN MFC APPLICATION LIKE PROG1 "MANUALLY" USING MICROSOFT DEVELOPER STUDIO-- 1. Use "File | New", choose "Win32 Application" as always, and enter a Project Name and Location as usual. 2. Enter the text for the header file (e.g., PROG1.H--see DECLARATIONS above) and for the .CPP source file (e.g., PROG1.CPP--see IMPLEMENTATION above). [Or copy these sources into the project's directory and use "Project | Add To Project | Files", entering the name of the .CPP file.] 3. Use "Project | Settings", and select the "General" Tab. From the "Microsoft Foundation Classes:" combo box, choose "Use MFC in a Shared DLL". 4. Build the project as usual. THE PROG1 DECLARATIONS (PROG1.H)-- PROG1.H just declares the CMainWin class derived from CFrameWnd, its constructor CMainWin() and its message map. Note that this simple application does not respond to any messages, so there are no message handler functions to declare. It then declares the CApp class derived from CWinAPP and declares that the InitInstance() virtual function base class member will be overridden. THE PROG1 IMPLEMENTATION (PROG1.CPP)-- 1. PROG1.CPP first defines the CMainWin constructor, which simply makes a call to the member function Create(). Whenever a CMainWin object is instantiated, this will cause a window with the title "An MFC Application Skeleton" to be created. Note that setting the first parameter to NULL causes most of the characteristics of the window that is created to take on their default frame-style values. (See online help on CFrameWnd::Create.) 2/3. PROG1.CPP next defines an empty message map using the macros BEGIN_MESSAGE_MAP() and END_MESSAGE_MAP(). Even though the application doesn't respond to any messages, we need to define the message map. In almost all MFC applications, there will be several message-mapping macros in the message map. 4. PROG1.CPP next defines the override to the CApp's InitInstance() member function. First a new CMainWin object is instantiated, and this gives us a pointer to the resulting window. Note that in our CMainWin constructor (see above) we made a call to Create(), which creates the window, so at this point our frame window has been created. We then use the pointer to the resulting window to invoke the ShowWindow() member function, which causes the window to display itself. And then the UpdateWindow() member function causes a the window to update its client area. Note that these are the same steps that were followed in API programming in the WinMain() function after the window was created by CreateWindow(). 5. Finally a CApp object is instantiated. As described in the "IMPLEMENTATION" section above, this causes the WinMain() [now buried in MFC] to execute, which, in turn, causes our CApp::InitInstance() to run--so the window appears. And, as described above, the message loop begins. When this application is built and run, we can visualize the following sequence of events occurring: CApp object is created --> MFC's WinMain() executes--> Registers class (default) Calls our CApp::InitInstance()--> Our override creates a CMainWin object--> The CMainWin constructor calls Create() which creates the window Our override calls the window's ShowWindow() function which displays it Our override calls UpdateWindow() which paints its client area WinMain() continues by calling its Run() function--> Which calls PumpMessage()--> Which starts the message loop. The MSG1 Example MFC Program (Mouse/Character Message Processing)-- This application responds to Left and Right mouse button presses by recording the current position of the mouse cursor and setting up a string to be displayed. It responds to key presses by saving the character in a string which will be displayed at the upper left hand corner of the client area. In either case, it forces a WM_PAINT message which is responded to by displaying the string at the correct location on the screen. MSG1 Declarations (MSG1.H file)-- The CMainWin class must contain the message handler functions the application is going to use to handle expose events, keyboard character entry, and left/right mouse button presses. These are, respectively, OnPaint(), OnChar(), OnLButtonDown(), and OnRButtonDown(), and must be declared in the .H file prior to the DECLARE_MESSAGE_MAP() statement. The implementation (.CPP file) must subsequently define these functions. MSG1 Implementation (MSG1.CPP file)-- Global variables (x,y) are declared to keep track of where the text is to be displayed in the window's client area. Their initial value is set to (1,1). Also a string (str) is initialized to "Sample Output". This will be what is displayed at (x,y) when the application starts (i.e., after the first WM_PAINT message). As the user moves/clicks the mouse or enters characters, the values of (x,y) and of the string will change. CMainWin constructor--defines a RECT structure to specify the position/dimensions of the window to be created. It then calls the Create() member function using the RECT structure to make the window. Note that here we are using more of the parameters of the Create() function to create the kind of frame window we want. CMainWin's message map--uses the message map macros associated with the messages this application is going to handle; namely, WM_CHAR, WM_PAINT, WM_LBUTTONDOWN, WM_RBUTTONDOWN. These macros are, respectively: ON_WM_CHAR(), ON_WM_PAINT(), ON_WM_LBUTTONDOWN(), and ON_WM_RBUTTONDOWN(). Next come the definitions of the message handler functions. Notice that in each case, Windows/MFC provides the necessary information as parameters in the functions (See online help for each of the message handler functions): OnChar(ch,count,flags)--Sent in response to the user pressing a key with a printable character. The first parameter contains the character code of the key pressed. Our override of OnChar() sets x and y to 1, then uses wsprintf() to put the character into the global string (str). Finally it invalidates the client area forcing a WM_PAINT message. The result is that whatever string is stored in str will be displayed at location (1,1) of the window's client area--its upper lefthand corner. OnLButtonDown(flags,loc)--Sent in response to the user pressing the left mouse button. The second parameter contains the CPoint (x,y) location where the button press occurred. Our override of OnLButtonDown() just sets the globals x and y to the x and y fields of the location specified in the loc CPoint structure and formats str to contain the string "Left button is down". It then invalidates the client area forcing a WM_PAINT message. OnRButtonDown(flage,loc)--Does the same as the previous function, but in response to the right mouse button being pressed. OnPaint()--Sent in response to any expose event (or an invalidation of the client area). Our override of OnPaint() creates a CPaintDC object (dc) connected to "this" window. The object's constructor performs a BeginPaint() and the destructor an EndPaint(). Since CPaintDC is derived from CDC, the object obtained (dc) gives us access to all the functionality in CDC, including the TextOut() member function. After creating dc, our OnPaint() handler function calls dc.TextOut(x, y, str, lstrlen(str)) to display the current contents of the global string at position (x,y) in the windows's client area. CApp's InitInstance() is defined to create and show the window, as in PROG1.CPP. Finally a CApp object (App) is instantiated, which causes execution to begin. USING RESOURCES WITH MFC (Menus)-- When an MFC application creates a window with a menu, the message map macro that is used to map WM_COMMAND messages to handler functions is ON_COMMAND(IDM_***,On***), where IDM_*** is the menu item identifier in the resource script (.RC file) and On***() is the handler function that you will need to write to respond to the user having selected the menu item. For example, if you had a menu item "Alpha" with an ID of IDM_ALPHA defined in the .RC file, the handler function (that you write) would normally be called OnAlpha(), and would, of course, be a member function of your CMainWin class. In this example, the message macro would be: ON_COMMAND(IDM_ALPHA,OnAlpha). As in API programming, we can set up a .rc menu resource script file with either a text editor or visually with the Developer Studio menu editor. If we do it visually, we'll have to edit both the resulting resource.h and the .rc files to get rid of the extra "garbage" inserted by Developer Studio's menu editor for use by its AppWizard (which we'll look at later). The resource.h file should only contain the #defines for the menu item IDs. The program's .rc file should only have the menu section and two #includes at the top: #include <afxres.h> #include "resource.h" All the other stuff should be edited out of the two files. We should add the .cpp source file and the .rc file to the project (as usual), and it should be built as in previous examples. The MENU1 example program-- This program has a menu with items "One", "Two", and "Help". The first two are popup menus with items "Alpha" and "Beta" under "One" and with items "Gamma" under "Two". Upon selection of any of these menu items an appropriate message box is displayed. The program also responds to left mouse button presses by displaying an "Abort/Retry/Ignore" message box. An appropriate message box is displayed depending on which button the user selects from the message box. MENU1.H-- Declares our CMainWin class with its the left button down handler function [OnLButtonDown()], its menu item handler functions [OnAlpha(), OnBeta(), OnGamma(), OnHelp()], its message map, and finally our CApp class. MENU1.RC-- This was created with the menu editor and edited to get rid of the AppWizard "garbage". This process also produced the resource.h file which was edited to get rid of its AppWizard "garbage". MENU1.CPP-- Defines the CMainWin constructor, which calls Create() to create the window. Notice that the "rectDefault" rectangle position and dimensions are used as well as the "MYMENU" name of the menu defined in the menu1.rc file. Defines the message map. As indicated above, the ON_COMMAND message macro is different from the other message map macros we've seen. The first parameter is the menu item ID (as defined in resource.h and used in menu1.rc); the second parameter is the name of the handler function that we will need to write. Here the names of the handler functions are: OnAlpha, OnBeta, OnGamma, and OnHelp. CMainWin's OnLButtonDown() handler function override--just puts up an "abort/retry/ignore" message box and stores the return value in the variable i. A case statement then puts up a simple "OK" message box with the caption of "Abort", "Retry", or "Ignore", depending on the value of i. Notice that since OnLbuttonDown() is a member function of CMainWin which is derived from CWnd, we can use the latter's MessageBox() member function. CMainWin's menu item processing functions [OnAlpha(), OnBeta(), OnGamma(), and OnHelp()] all work the same way. They just use MessageBox() to display a message box with the appropriate caption and contents. USING MODAL DIALOG BOXES IN MFC-- Recall that a dialog box is a child window that contains one or more child window controls. The dialog template is given in the resource (.rc) file. Under the Win32 API, a dialog box is created with the function DialogBox() or DialogBoxParam(). The dialog box has its own message processing function with a switch/case statement to handle messages from the various controls it contains. Under MFC, dialog boxes are encapsulated by the CDialog class (derived from CWnd). So when we require a dialog box, we should derive our own dialog box class from CDialog. The dialog box message handling is done the same way as for the main window--namely by using message maps. So that inside your dialog box class declarations (.h file) you will need to declare the message handling functions and a message map. The implementation (.cpp file) will have to define the dialog box class message map and the handler functions. To create a dialog box, the constructor of the class derived from CDialog should call the CDialog constructor. The arguments are the name or ID of the dialog box (as specified in the .rc file) and a pointer to the owner window. If any initialization needs to be done as the dialog box is created, the code that does it should be placed in CDialog's OnInitDialog() handler function which is invoked in response to the WM_INITDIALOG message. To activate the dialog box use CDialog's DoModal() member function. As in the Win32 API, DoModal() does not return until the dialog box has been closed. Until then, all messages from the dialog box controls will go to the dialog box Message map handler functions. CDialog's EndDialog() member function is invoked to close the dialog box and thus cause DoModal() to return. The DIALOG1 Example Program-- The menu has "Dialog" and "Exit" items. If the user selects "Dialog" a modal dialog box containing the buttons "Red", "Green", "Cancel" and "OK" is displayed. If the user chooses "Red" or "Green", a message box with appropriate text in it appears; if either "Cancel" or "OK" is chosen, the dialog box closes. It the user selects the "Exit" menu item, the window is closed. DIALOG1.RC and RESOURCE.H--These files were prepared with the Developer Studio menu editor and the dialog box editor. As in the MENU1 example the "garbage" was edited out afterwards. Note that the default ID's for the dialog box "OK" and "Cancel" buttons were used. CDialog provides default handlers for both IDOK and IDCANCEL WM_COMMAND messages. DIALOG1.H--What is new here is the declaration of our CSampleDialog class, derived from CDialog. The constructor simply specifies that the CDialog base class constructor is to be used with the arguments provided when the CSampleDialog object is created. We declare handler functions for three of the buttons inside the dialog box: OnRed(), OnGreen(), and OnCancel(). [The default handler in CDialog for the "OK" button will be used, so we don't need it in the message map.] DIALOG1.CPP--After defining the main window's message map to take care of WM_COMMAND messages from the two menu items, we define the two handlers: OnDialog() and OnExit(). In the first case a CSampleDialog object (dialOb) is instantiated, and its constructor invoked passing it the name of the dialog box and "this" to indicate that this window is the owner. Then the resulting object's DoModal() function is called to create the dialog box. In the case of OnExit(), we simply cause the CMainWin to send a WM_CLOSE message by invoking SendMessage(). This, of course, goes to the main window's message map and causes Windows to close the main window and terminate the application. The next section of the program sets up the dialog box message map, specifying that OnRed(), OnGreen(), and OnCancel() are the handler functions for messages from the buttons inside the dialog box. We then define the three functions. In the first two cases, message boxes are displayed; in the case of OnCancel() a call to EndDialog() is made which causes the dialog box to close. Notice that the default handler for the IDOK message does the same thing automatically. DIALOG BOXES CONTAINING LIST BOXES-- As you will recall, list boxes are controls that both generate and receive messages. An application can send a list box messages to fill it with strings (LB_ADDSTRING), get the current selection (LB_GETCURSEL), get text item (LB_GETTEXT), etc. The list box sends messages in response to the user selecting items, etc. Under the Win32 API we use SendDlgItemMessage() to send these messages to the list box. UNDER MFC, messages can be sent to a list box (and other controls) in a dialog box by using member functions of the classes from which the list box (or other control) is derived. For example, for a list box, the CListBox class has member functions like AddString(), GetCurSel(), GetText(), etc. (Refer to the online help.) As in API programming, to send a message to a control in a dialog box, we must identify which control us being accessed. Under API this is done by using the function GetDlgItem(). Under MFC, the CWnd class has a similar member function with the same name that returns a pointer to the control. Each time we access the control we must retrieve the pointer returned by: CWnd::GetDlgItem(CONTROL_ID); Since there are many possible controls, the result must be type cast to the type of control object being obtained. In the case of a list box control, that means using code like: CListBox *plistbox = (CListBox *)GetDlgItem(CONTROL_ID); Once this pointer is retrieved, it can be used to access any of the member functions that send messages to the control. For example: plistbox->AddString("some string"); would cause "some string" to be added to the list box. To initialize a list box, the WM_INITDIALOG message handler, OnInitDialog(), should be overridden so that it adds strings to the list box when it is first created. That means OnInitDialog() must be declared in the section of the .h file that declares our CDialog class. In the implementation (.cpp), our OnInitDialog() should first call the base class OnInitDialog() so that initialization is done correctly. Our OnInitDialog() can then make calls to AddString() to fill the dialog box. The DIALOG2 Example Program-- In this application, when the user selects the "Dialog" menu item, a dialog box with a fruit selection list box appears. If the user double clicks on an item or selects an item and then presses the "Select Fruit" button, a message box appears with the selection and its position in the list box. So in response to either IDD_SELFRUIT (the button) or LBN_DBLCLK of the IDD_LB1 list box (the list box notification code generated when the user double clicks on an item in the IDD_LB1 list box), the OnSelect() handler specified in the dialog box message map is called. This function makes calls to GetDlgItem() to get a pointer to the list box, then invokes GetCurSel() and GetText() to retrieve the item selected.