Author: Brian Martin
Lesson 1: Behind the scenes, Handles and Messages
Though you think you want to dive right into the code, you really don't. Windows programming is overwhelming at first. Let's take a quick look at how Windows works. The backbone of all of your programming will be responding to and sending messages. What are messages? Messages are simply a 32bit number designating some event. Example: You move the mouse, a message (defined as WM_MOUSEMOVE) is 'posted' to the active window. You press a key, a message (WM_KEYDOWN) is 'posted' to the active window. You resize the window, a message (WM_SIZE) is 'posted' to the active window. Get the picture?
Now where do these messages go? They get queued up and a window eventually takes them out of the queue and reacts to them. For instance when a window gets the WM_MOVE message it changes the coordinates of the window and redraws it on the screen.
Let's move on to Handles. Windows is very much object oriented. You have several window objects (like the desktop, the program your reading this with, etc...). How does the programmer distinguish all of these things in an non-object-oriented language? He uses handles. Handles are a way to reference different windows objects. You can have handles to windows, handles to files, handles to allocated memory, handles to images, etc. You can think of them as pointers. You must create them some how. And when you are done with them, you must destroy them. If you don't you will end up with what is called a resource leak. This could bring your system to a grinding halt. So take care to always make sure they are destroyed at sometime.
Now lets tie these two things together. Say you have a window. You will have a handle to it (called an HWND). Lets name your handle your_HWND. The operating system wants to tell you to redraw your window because it was just uncovered by some other window. Windoze passes you a message like this:
PostMessage(your_HWND, WM_PAINT, 0,0);
This function posts a paint messages to the window with handle your_HWND. The last two parameters are used for extra information about the message. Don't worry about them for now.
Now your application will have a function with a big case statement in it to handle all of the messages. For example:
void HandleTheMessage(long Message) { switch(Message) { case WM_PAINT: DrawWindow(); break; case WM_KEYDOWN: break; etc... } }
Ok that is basically how windows works under the hood. That should be enough to get you going when we start talking about MFC.
Lesson 2: C++ Essentials
If you want to use Microsoft Visual C++, it helps a ton if you really know C++. Everything is about classes. If you are used to plain C, you won't really see the big deal with classes until you use them for a while. Let's review what you need to know about classes to get started with VC++.
A class is a structure for the most part. Let's work with an example instead of me just telling you rules. Let's make a class to represent a line. In the .h file you would define the class as follows:
class CLine { int m_nX1; int m_nY1; int m_nX2; int m_nY2; public: // constructors CLine(); CLine(int x1, int y1, int x2, int y2); // destructor ~CLine(); // set the line data void SetPoints(int x1, int y1, int x2, int y2); // draw the line void Draw(); }
A quick word about naming conventions. Class names usually start with 'C' and the member variables usually are prefixed by a 'm_'. Then in the microsoft way you will have a letter to let you know what data type the name is and then the name of the variable. Capitalize the letter of all new words in the name. Don't use underscores and stuff like that. You may have the false belief that your coding style is better. From experience I can tell you that the microsoft way is the way. It makes things easy to read and easy to remember names (even when it is someone else's code). If you see m_pPoint, you would assume this is a member variable of a class that points (it is a pointer) to a point. If you see fData, you would assume that it is a floating-point value.
Back to our class. The int variables are the end points of the line. Note that they are before the 'public:' part. This means that a programmer using this class will not be allowed to manipulate these guys directly. They are not for 'public' use. The functions under the public statement are for public use. The first three are called constructors. These functions are called anytime a new CLine class is created. Here are some examples when the are called:
// this calls CLine() CLine MyLine; // this is a pointer to a CLine class CLine *pMyLine; // this calls CLine() pMyLine = new CLine; // this is a pointer to a CLine class CLine *pMyLine; // this calls CLine(int x1, int y1, int x2, int y2) pMyLine = new CLine(0,0,10,10); // this calls CLine(int x1, int y1, int x2, int y2) CLine MyLine(0,0,10,10);
All of these construct a line. Some initialize it to its default settings and others copy coordinates. The 'new' keyword is used to create new things in C++, like malloc in C. You need to call 'delete' for everything you say new to, like free in C. This goes for classes as well as other data types. I could allocate an array of 100 integers with:
// a pointer to some integers int *pNumbers; // make memory for 100 of them pNumbers = new int[100]; // set the first element to 0 pNumbers[0]=0; // set the last element to 99 pNumbers[99]=99; // free the memory. delete [] pNumbers;
Notice the [] after the delete. This is to tell the program to delete the entire array. If you say 'delete pNumbers;' you will only free memory for the first element. You will then be 'leaking' memory. Memory leaks are when you forget to delete memory. This may end up crashing your computer if you use all the computers memory.
Sorry, let's get back to the constructors for CLine. The code for these constructor functions which automagically get called when a new line is created will look like:
CLine::CLine() { m_nX1=0; m_nX2=0; m_nY1=0; m_nY2=0; } CLine::CLine(int x1, int y1, int x2, int y2) { m_nX1=x1; m_nX2=x2; m_nY1=y1; m_nY2=y2; }
Notice that the function declaration is much like a regular 'C' function except that we put the class name and two colons in front of the function name (CLine::). One difference with constructors is that they don't have a return value. This is the case for destructors also. A destructor is the function which automagically gets called when our CLine is deleted or goes out of scope. For instance:
// this is a pointer to a CLine class CLine *pMyLine; // this calls CLine() pMyLine = new CLine; // memory for the class is cleared up and ~CLine() is called delete pMyLine; { // this calls CLine() CLine MyLine; } // this '}' ends the section of the program where MyLine is // valid. ~CLine() will be called. (MyLine goes out of 'scope')
For our class, ~CLine() doesn't need to do anything. However, sometimes you may want to put your cleanup code here. Like deleting any allocated memory in your class. Since we have nothing to do out function code is empty:
CLine::~CLine() { // do nothing }
Let's fill in the other 2 functions.
void CLine::SetPoints(int x1, int y1, int x2, int y2) { m_nX1=x1; m_nX2=x2; m_nY1=y1; m_nY2=y2; return; } void CLine::Draw() { // psuedo code here, these are operating system functions to draw a line MoveTo(m_nX1, m_nY1); LineTo(m_nX2, m_nY2); return; }
How would I call these functions? Here are a couple of examples. One with pointers and one without.
CLine *pLine = new CLine(0,0,10,10); pLine->Draw(); delete pLine; CLine MyLine; MyLine.SetPoints(0,0,10,10); MyLine.Draw();
That's it for the class. Now this class can be used in other classes. You can imagine a CSquare class that has 4 Cline classes in it:
class CSquare { CLine m_LineTop; CLine m_LineLeft; CLine m_LineBottom; CLine m_LineRight; ... }
Or better yet, the point of all of this class stuff, you can use the CLine class to make your own class. This is done a ton in Visual C. Lets say you wanted to draw lines in your program, and you thought the line class might be nice, but it is missing an important feature, it doesn't let you set the line color. Instead of writing a whole new class, you can simple inherit the CLine class. This would look like this:
class CColorLine : public CLine { public: void Draw(long color); };
What's going on here? Well with this class we have all the functionality of our other class, but now we can use this other Draw() function which allows us to set the color. The CPP code would look like:
void CColorLine::Draw(long color) { // psuedo code here, these are operating system functions to draw a line SetColor(color); CLine::Draw(); return; }
Now we have all the functionality of the other class but we added an extra function called Draw. But it's the same name as our other Draw! No matter. Cpp is smart enough to know that if you call Draw(color) to use the new function, but if you call Draw() it will use the old function. The strange part of the code may be CLine::Draw(). This just tells our program to call the base class's Draw function. We save ourselves from having to write that LineTo and MoveTo code again. Pretty cool, huh? Now we can do something like this:
CColorLine MyLine; MyLine.SetPoints(0,0,10,10); // assuming 0 is black, this will draw a black line. MyLine.Draw(0);
Of course I'm leaving out a ton of aspects and things here. Like defining operators, overriding functions, virtual functions, protected and private members... the list goes on. You have enough to get started though.
Lesson 3: The workspace, the VC++ IDE
Windows programming is tricky stuff. Don't let anyone fool you. But microsoft has blessed us with their IDE called Developer's Studio. It will handle all the compiling and linking, provides help, fills in tons of code for you, and gives you that 'visual' designing environment to make cute little dialogs.
There a few things you must learn to use to get anywhere. First and most importantly, USE THE ONLINE HELP. There are so many windows functions, you will find yourself in the help 50% of the time or more for the first few months. If you have visual c 6.0 you need the MSDN for help. Get it. If you have 4.2 or 5.0 you can use the built in help.
Next, the single most important key combination is Ctrl-W. This brings up what is called the 'Class Wizard'. This guy will insert a bunch of code in your project for you. It handles all the code for connecting functions up to the messages windows posts.
You will also find yourself in the resource view a bit. It is used to design the dialogs. You can drop buttons on them and what not. With a couple of clicks here and there you can layout your whole application. Then using the class wizard, you can have your project 50% finished. All is left is to fill in the functions the class wizard made to handle the button click messages and what not.
Now you need to know how to start a new program and we can begin with the real code. Go to the File menu and select New. Then go to the 'Projects' tab and select MFC AppWizard. Type in a name for you application and press ok. The only important option for now is the first one. This is the type of application you want to build. The simplest is a 'Dialog Based'. Try them all, but try dialog based first. Just select all the defaults afterwards by pressing 'next' and 'ok'. You'll end up with a few files, but lets ignore everything.
Go to the resource view and plop some buttons and stuff on the dialog. Just play around. When you feel like it, Build the project from the 'Build' menu and run it (I always just press Ctrl-F5, it builds the project and runs it). Everything should build and run. Wa-la. Application complete and running.
One last tip on the IDE. If you right click on a menu bar, a dialog will pop up. You can select 'Customize'. This is very useful. You can build one custom menu with all the buttons you need on it by simply dragging the buttons to and from the menu bar. You can even change the menu layout (like by putting the File, New as a button on the tool bar). One really needed control is under the Build category in the customize dialog. It is the drop list used to select the active configuration. Drag that sucker up to your main toolbar and change it's size still it looks good to you. I have only one toolbar on my screen and it has the following buttons on it: save, save all, compile, build, stop build, rebuild all, set active configuration, find, find in files, show view, show output window, set break point, remove break point, tile windows, find in help.
Lesson 4: MFC
Are you ready to start programming? No you are not. You don't want me to teach you a stupid 'hello world' application, do you? If you want to make the most of Visual C++ you have to use Microsoft Foundation Classes (MFC). These classes are great because they wrap all of those handles we talked about in the first lesson with easy to use classes. The most important to you right now is the class CWnd. This wraps the functions which needed a window handle (HWND). Remember that PostMessage function I mentioned?
PostMessage(your_HWND, WM_PAINT, 0,0);
Well now we can take our windows class and call it's member function.
MyCWnd.PostMessage(WM_PAINT, 0,0);
This does the same thing, but now we don't have to keep track of the window handles. But don't be fooled, they are still there. We can still use them too. They are now just member variables of the class. The handle to the window a CWnd is associated with in the member variable m_hWnd. We can call the old post message this way:
::PostMessage(MyCWnd.m_hWnd, WM_PAINT, 0,0);
Those colons (::) are used to tell MFC that we are calling the old fashioned versions of the function. You can get away without using them most of the time, but I put them here so you won't be too confused when you see them in microsoft's code.
The CWnd class is the base for several other classes. Like CButton and CDialog which I hope the names make self explanatory. From CButton you can access the windows handle also. (You'd be surprised how many things are windows.. Scroll bars, edit boxes, tree views, the desktop...) Got all that?
Good.
The next most important class, though you won't explicitly use it much, is CWinApp. This class is the backbone of all of your future MFC applications. This is the class that does the main dirty work under the hood. Your program will have a CWinApp class, which when created, starts the program execution. The main function called when the CWinApp is constructed is InitInstance(). In this
function windows are created and the application is set up. Think of the InitInstance() function in CWinApp as the main() section in a C program.
Let's move on to one last very useful MFC class that you will surely use: CString. This is one of microsoft's support classes. It is used to make string manipulation easier. Since CString overrides many of the common operators, like = and +, you can do things like this:
CString strMyString; strMyString="May the Force be with you"; strMyString+=" young Jedi." printf("%s", strMyString); //output will be "May the Force be with you young Jedi.";
Lesson 5: Dialog Applications
We won't build a dialog application just yet, but I will tell you enough here so that you get the picture of what's going on in dialog applications. Dialog apps are the simplest apps in my opinion. In the IDE, go to File, New, Projects, MFC AppWizard(exe), and type in a project name. Hit next. Select Dialog Application as the type of application and then hit finish. Next go to the File View. You will see the source files created automagically. You should be able to compile and run the application as it is.
What is going on in all these files? Everything boils down to the CWinApp derived class and the CDialog derived class (which is derived from CWnd). Look in the source file named after your project. You should see a the InitInstance() function there. Inside of that function you can see that a dialog class is constructed, it is set as the 'main window' of the application, and it is displayed with the DoModal() function. Once you exit your dialog app, the DoModal() function returns and your dialog is hidden. InitInstance() returns FALSE and your application ends. Now the question is, "What is DoModal()?"
There are 2 ways to create a dialog, Modal and Modeless. A Modal dialog suspends the program until you press OK, Cancel, or Close. On the other hand a modeless dialog can remain opened while allowing you to press buttons and whatnot on the rest of the application. An example of a Modal dialog is one of those annoying error boxes with an OK button on it. That is the only type of dialog we'll talk about here. To create a Modal dialog, you simply need to call the dialog's DoModal() function. It returns either IDOK or IDCANCEL depending on how you exited the dialog. Internally the DoModal() will call OnInitDialog() which is a good place to initialize your dialog variables. If you create a dialog app, you will notice that a default dialog class and resource is created for you. The file name for the class will be your project name with a 'Dlg' tacked on at the end.
Though we aren't making an application just yet, I have to tell you how to put something useful on the dialog. First open up the resource view. Open up your dialog in the editor. Right click on the dialog and select 'properties'. Make sure the 'Visible' checkbox is checked. If it isn't, you won't be able to see your dialog. (Remember this, it will come back to haunt you in the future). You can change the Dialog's caption here as well as other things.
Now drag a button control onto the dialog somewhere. Right click on it and select properties. You can change the ID of the button to something more descriptive like IDC_SHOWMESSAGE. You can also change the text on the button to something more descriptive like 'Show Message'. You have a button now. But it does nothing. You can easily fix that. Press Ctrl-W to bring up the class wizard. Make sure you are on the first tab. You should see your dialogs name and the button's ID in the list on the left. If you select the dialog's name you can see all of the functions and messages that the class wizard will let you add code for on the right. You can see the WM_PAINT and all the other messages we talked about. If you select the ID of the button you can see the messages that the button sends. Double click on the CLICK message and accept the default function name. You see that it appears in the list at the bottom. Now double-click on the function in the bottom list. Shazam, you are transported right to the cpp file where you need to fill in the code. Let's do something easy. Just add the line:
AfxMessageBox("Stupid Text");
Compile, build and run your application (just press Ctrl-F5). If you press the button, you should see a message box pop up when you press the new button. (There are some Afx... functions that are useful. I think the message box one is the most useful out of all of them. It is instant feedback).
That was dead simple. You see how to add message handlers now (like for the button click), but you need at least one more vital bit of information to make a useful dialog box. How to use the automatic data handling stuff in MFC. This is best described by going back to the code again. Bring up the resource editor one more time and this time add an Edit Box to your dialog. Again right click on it and give it a nice and friendly ID. Hit Ctrl-W and bring up the class wizard. This time go to the second Tab. Here is where you add member variables that are associated with the controls on your dialog. Double click on the Edit Box's ID. You now have the choice to add a variable to your project. Give it a name like m_strMessage since it will be a string for our message box. Make sure the data type selected is CString there at the bottom. Press OK. And press it again to get out of the class wizard.
When you add a member variable to a dialog like this, you can set the value to that in the control of the dialog by calling
UpdateData(TRUE);
Likewise you can change the data displayed in the dialog to represent your variable's value by calling
UpdateData(FALSE);
Let's put this to use and finish up this lesson. Go towards the end of your OnInitDialog() function and in it put the two lines:
m_strMessage = "Initial text"; UpdateData(FALSE);
Then go to the function which is called when the button is pressed and replace the AfxMessageBox() line with these two lines:
UpdateData(TRUE); AfxMessageBox(m_strMessage);
Ok, we are done. What we just did was set the initial text in the edit box to "Initial text" with the UpdateData(FALSE). Then when the button is pressed the text in the message box is displayed since we get the text from the dialog with UpdateData(TRUE).
By playing around and by reading the help (or a good book) you will learn how to use the other controls. On of the trickiest to figure out with out help is the slider bar. If you use one of these you will have to handle the dialog's scroll messages. That's just a hint for you.
Lesson 6: SDI and MDI Applications
We are getting to some advanced stuff now. In this lesson I am not going to go in depth at all. I will just give you a flavor of the structure of a SDI (single document interface) and a MDI (multiple document interface) application. In the last lesson we will build a SDI application and you can see the nitty gritty there.
The SDI application is typically used when you intend to work with only one data set at a time. For instance, the program notepad.exe is a SDI application. Netscape is also an SDI application. At any one time, there is only one document open at a time. Word for Windows and the VC++ developer studio are MDI applications. In these you can have several documents opened at once. This is particularly useful when you want to cut and paste between documents. Another use for MDI applications is to have one document, but several different views open that view the data differently. A graphing application comes to mind where in one window you have a spreadsheet-like data list, and in another window you have a plot of the data. For small applications, a SDI application will usually be all you need. After you master it, the jump to MDI is a snap. Let's go over the structure of a SDI app.
Remember that in a dialog app, we had just two main classes. CWinApp and CDialog. Here again we have a CWinApp which serves the same purpose as it did in lesson 5. The CDialog class however is replaced by 3 other classes: CMainFrame, CDocument, and CView.
CDocument is a class that has no display, and typically doesn't react much with the messaging system of windows. It is used as a class to manage your data. MFC will create the code automatically which handles the event of File->Save, File->SaveAs, File->Close, and File->Open. All you need to do is to fill in the blank functions in the CDocument class.
Next is the CView. Most likely you will spend more time writing code to display and interact with the document's data then you will writing any other code. This is where the CView comes in. The CView is a class derived from CWnd, which is used for displaying your CDocument in some way. It is also one of the places where you can handle events like mouse clicks and what not. The heart of the CView is usually a call to get a pointer to your document followed by some drawing routines to display the data in your document.
The CMainFrame acts as a way to bridge the gap between your document/view classes and the rest of the application. Do you see that frame which goes all around applications boarders? That is the Main Frame window of the application. The title bar, the menu, the scroll bars, the status bar, and the tool bars are all part of the main frame window in an SDI application. You typically put the code to handle these objects in the CMainFrame class. The CMainFrame class is the main window of the application. The CView class is typically a child window of the CMainFrame. (For the most part, the child/parent window relations just tell windows what windows are 'stuck' to what other windows. If you move a parent window, all of the children will move also. If you destroy a parent window, all of the children will be destroyed. Etc.)
You should have a pretty good idea now of how SDI applications are constructed. MDI applications are similar, but there can be several CDocument classes in existence at the same time and there is an added class called CChildFrame which acts as a connection between the CView and the CMainFrame.
Lesson 7: Making a Data Viewer
The moment you have been waiting for, we finally will make a useful application. If you have just skipped the last 6 lessons, then you will probably be able to follow along, but you may not really understand what you are doing. (But since you are the type of person that skips ahead, you are probably used to this.)
I have decided to make this example a data viewing application that takes a text file of data, reads it in, and then displays it. If that isn't enough, we are then going to use the timer to animate the data. Let's first assume that we are doing an experiment tracking the random motion of a drunken bug on a table. Every second we measure its distance from two adjacent sides of the table. These are what we will call the bug's x,y coordinates. Our data file looks like this:
390.789 362.245 386.032 366.429 386.559 369.289 385.557 370.483 384.841 372.370 385.785 371.975 389.348 371.005 377.266 379.550 376.916 382.096 373.959 384.111 373.109 384.387 370.598 382.973 370.067 383.667 369.099 377.171 366.549 379.162 368.245 383.977 366.427 385.877 364.343 388.575 365.326 389.769 368.751 389.556 369.598 386.514 389.381 384.817 387.311 381.979 388.205 382.978 386.632 387.414 385.150 388.393 384.099 390.620 382.926 394.712 385.771 396.611 375.693 393.622 376.697 392.655 394.063 397.035 391.727 401.327 379.119 400.460 381.912 407.491 384.119 407.505 383.090 406.474 384.888 408.943 386.664 409.806 386.207 409.759 388.031 411.599 387.911 411.545
Go cut and paste this to a file named BugPosition.dat if you want to follow along. We first fire up Visual C++ Developers Studio and create a new project. In this case I called the project 'BugTracks'. For the app wizard options select SDI application. Keep the default settings for the rest of the choices, EXCEPT deselect printing preview and docking toolbar.
First lets figure out how to get our data into the program. Go to the ClassView and double click on your document class. It should be called CBugTracksDoc if you named your project 'BugTracks'. When you double click on the class name, the .h file will be opened in the editor. Right before the declaration of the class, lets declare a structure to hold the data.
struct SBugData { float x; float y; };
Also I want to use one of microsoft's template classes. Templates are C++ things that allow you to write a function for an arbitrary data type. The one I like to use is the CArray class. Include the afxtempl.h before your structure declaration:
#include
and then in a public area of your class declare a CArray template class as follows:
CArraym_BugDataArray;
Yea, it looks funny, but that is the way you want to type it. This is sort of like declaring a data type of CArray but we have to tell the template class what type of data we want to store in the array. That is done between the brackets <,>. The first thing in the brackets is the type of data we want to put in the array. The second thing is what we will pass to the CArray class when we want to add a new element. Since we have a list of data points, it is obvious that we want to have an array of SBugData. The second parameter is also SBugData meaning we will just pass the data to the array. (Alternatively we could have passed a 'reference' to the data, but that is another lesson).
Let's go to the .cpp file for the document and add the code now. Expand the CBugTracksDoc in the ClassView. You should see the member functions for the document. Double click on OnNewDocument(). You will jump to the function in the .cpp file. This function is called every time a new document (file) is opened. All we want to do here is to clear out the array so it will be ready for the new data. Where you see the //TODO comment, add this line of code:
m_BugDataArray.RemoveAll();
Now to fill up the array, jump to the Serialize() function. This is a function called when a new file is opened or saved. Instead of the good old FILE pointers you use in C with fopen, we are going to use microsoft's CArchive class. You will notice that a CArchive is passed (by reference) to the Serialize() function. This class has the same functionality as we get with the fread and fwrite functions.
void CBugTracksDoc::Serialize(CArchive& ar) { // if not storing the data, read it if (!ar.IsStoring()) { SBugData Data; CString strOneLine; // read data in, one line at a time while(ar.ReadString(strOneLine )) { // convert the text to floats sscanf(strOneLine,"%g %g\n",&Data.x,&Data.y); // add the data to the array m_BugDataArray.Add(Data); } } }
For the very basic display, we just need to add some code to draw the data. Go to the function OnDraw() in the view class CBugTracksView. This is the function that is called every time the window needs refreshed. All drawing is done through the CDC class. The CDC class has several drawing functions, here we will only use the MoveTo and LineTo calls.
void CBugTracksView::OnDraw(CDC* pDC) { // get a pointer to the document class CBugTracksDoc* pDoc = GetDocument(); // get the total number of data points int N=pDoc->m_BugDataArray.GetSize(); // draw all of the connecting lines for(int i=0; i< N-2; i++) { pDC->MoveTo(pDoc->m_BugDataArr ay[i].x, pDoc->m_BugDataArray[i].y); pDC->LineTo(pDoc->m_BugDataArr ay[i+1].x, pDoc->m_BugDataArray[i+1].y); } }
Well that is it! Compile and run the program. You will get a few warnings since our data is float, but screen coordinates are int, but that is harmless in this case. Once the program is running, go to File, Open and select the data file we created above. It should display the track in the lower-middle part of the window. We could call it quits here, but let's add a couple of more features.
First, I hate the File, Open menu. Lets make our application accept files that are dropped on the main window. Go to the InitInstance() function in our CWinApp class CBugTracksApp. Near the end of the function add this line:
// Enable drag/drop open m_pMainWnd->DragAcceptFiles();
Now let's take advantage of the status bar and put some useful text in it. The status bar is managed by the CStatusBar class, which is a protected member of CMainFrame. This means that we can't touch it from other classes. We can either move its declaration to a public part of the class or just a public member function to CMainFrame to change the status bar text. We will do the later. Right click on CMainFrame in the class view and select 'Add Member Function'. A dialog will pop up to help add the member function. Type in "void" (without the quotes) for the function type -- this is the return value of the function, and type in "ChangeStatusText(LPCTSTR text)" as the function declaration. Make sure that the 'access' is set to public. Press OK. This will automagically add the declaration to the .h file and a blank function to the .cpp file of CMainFrame. The LPCTSTR is one of many microsoft defines for data types. We could have alternately have typed "ChangeStatusText(const char *text)". LPCTSTR stands for Long Pointer to a Constant T STRing. A T-string is just a string that will work on computers with different character sets (like Japanese). On computers in the US, a T-string is just the same as a char *.
Jump to the new function in the CMainFrame .cpp file and add the code to change the text on the status bar. To do this we'll just use the CWnd function SetWindowText. CStatusBar is derived from CWnd so we can always use any of the CWnd functions with it. A hint on how to find out about all of these strange new functions... use the help and look at the 'class members' for the class, and then look at the class members for all of the base classes from which it was derived. Your function should now look like this:
void CMainFrame::ChangeStatusText(LPCTSTR text) { m_wndStatusBar.SetWindowText(t ext); }
We have to call this function from somewhere, and I'll do it from the document. Go to the 'File View' which is the view I use most. Under 'Source Files' double-click on your document file "BugTracksDoc.cpp". Go to the top of that file and include the header file for CMainFrame right after the rest of the includes so that we can access the new function we just made.
#include "MainFrm.cpp"
Next go to our Serialize() function and modify the reading code to spit some text out to the status bar. We first get a pointer to the main window, which is the CMainFrame window in SDI applications. Since the function AfxGetMainWnd() returns a CWnd*, we cast it to a CMainFrame*. Then we use the CString's Format function and CArray's GetSize function to create a text string for the status bar.
void CBugTracksDoc::Serialize(CArchive& ar) { if (!ar.IsStoring()) { SBugData data; CString line; CString strStatus; // get a pointer to the main window // (which is the mainframe for SDI applications) CMainFrame *pMain = (CMainFrame*) AfxGetMainWnd(); while(ar.ReadString(line)) { sscanf(line,"%g %g\n",&data.x,&data.y); // tell the user your reading points strStatus.Format("Reading point %d",m_BugDataArray.GetSize()); pMain->ChangeStatusText(strSta tus); m_BugDataArray.Add(data); } // tell the user the total number of points strStatus.Format("Loaded %d points.", m_BugDataArray.GetSize()); pMain->ChangeStatusText(strSta tus); } }
If you run the app, you'll notice all of the default menu items. We don't need most of these. Let's clean up the menus and add an item for animating the bug track which we'll code later.
Go to the Resource View. Under the Menu resource, double click on IDR_MAINFRAME. This is the menu resource for the main window. In the edit window, click on File. Then delete the menu entries for New, Save, and Save As. Also delete the main menu headings for Edit and View. Next, go to the empty box at the end of the menu and Add a new heading called 'Track' by selecting the empty box and typing 'Track'. Drag the Track menu heading so that it is between File and Help. Click on the Track menu and then click on the empty sub menu box. Type in '&Animate\tAlt-A'. The & underlines the 'A' in Animate so that it is the menu Hot Key. The \t is just the scan code for a tab and the Alt-A will be our hot key to start the animation. For the ID, type in `ID_TRACK_ANIMATE', though this will be filled in automatically if you ever forget.
In order to make Alt-A our hot key, go to the Accelerator resources and double-click on IDR_MAINFRAME. In the edit window, double-click on the empty box at the end of the list. From the drop list for the ID, select the ID of your new menu item (ID_TRACK_ANIMATE). Press the `Next Key Typed' button and then press Alt-A. Hit enter to close the dialog.
Before we are done with resources, you should modify the icons to something more suitable for this app. I'm sure you can figure out how to do this. The only hints here are to make user and modify the 32x32 sized icon AND the 16x16 sized icon. If you want part of the icon to be transparent, use that greenish color with the two borders around it on the color palette.
Now we can get back to coding. It's time to add fancier drawing and animating. We will animate the bug track by drawing more and more segments of the path in red as time increases. The rest of the path will be drawn in black.
In order to keep track of the last segment in the path that is to be drawn in red, we have to add a member variable to our document. Go to the Class View, right click on the document class, and select Add Member Variable. Type in 'int' as the data type and 'm_nBugPosition' as the variable name. Make sure that it is public and press OK.
Jump to the OnNewDocument() in the document class. Add this line to initialize the new variable to -1. We will use the value -1 to designate that the track is not being animated.
m_nBugPosition = -1;
Next let's add the message handler for our 'Animate' hot key and menu. Press Ctrl-W to bring up the class wizard. In the class name drop box select the view class (CBugTracksView) and in the Object ID list, select the ID of our new menu and hot key command (ID_TRACK_ANIMATE). You'll see the two possible choices in the Messages list. Double-click on COMMAND to add a function to handle our new command. You will be prompted for a function name. Just accept the default one OnTrackAnimate() and press OK. You will see the function appear in the Member Function list near the bottom of the dialog. Double-click on the function to jump directly to the code. We set m_nBugPosition to zero and start a timer that will redraw the bug tracks in intervals of 0.2 seconds.
void CBugTracksView::OnTrackAnimate() { // get the document CBugTracksDoc* pDoc = GetDocument(); // set the position to the first data point pDoc->m_nBugPosition=0; // create a timer with id=1 and delay of 200 milliseconds SetTimer(1,200, NULL); }
Next we need to handle the timer message. Ctrl-W back to the class view. Make sure you are looking at the view class, select the class as the Object ID, then double-click WM_TIMER in the message list to handle the timer message. Again, double-click on the function name to jump to the code. In the OnTimer function we will first check the ID of the timer to make sure we are responding to the correct timer. In this case we set the timer ID to 1. Then we will invalidate the window so that it will be repainted.
void CBugTracksView::OnTimer(UINT nIDEvent) { if(nIDEvent==1) { // tell windows the view needs redrawn // note: the last parameter is the erase flag. // if it is TRUE, things will flicker like crazy. InvalidateRect(NULL,FALSE); } CView::OnTimer(nIDEvent); }
All that is left now is to fix up the OnDraw() function in the view class. We need to first draw the red tracks, then the blue ones, then increment the position m_nBugPosition. If m_nBugPosition is larger than the number of positions we will set it to -1 and kill the timer.
One of the new things in this code is the CPen class that is needed to change the color of the line. The way these graphical objects work is that you 'select' the object in to the CDC class. When you are done with it, you select the old one that was in there previously and delete the one you just used.
void CBugTracksView::OnDraw(CDC* pDC) { CBugTracksDoc* pDoc = GetDocument(); ASSERT_VALID(pDoc); // make pens for solid lines of thickness 2 CPen RedPen(PS_SOLID, 2, RGB(255,0,0)); CPen BluePen(PS_SOLID, 2, RGB(0,0,255)); CPen *pOldPen = pDC->SelectObject(&RedPen); int i, N=pDoc->m_BugDataArray.GetSize(); // draw any tracks which need animated for(i=0; i < pDoc->m_nBugPosition-1; i++) { pDC->MoveTo(pDoc->m_BugDataArr ay[i].x,)pDoc->m_BugDataArray[ i].y); pDC->LineTo(pDoc->m_BugDataArr ay[i+1].x,pDoc->m_BugDataArray [i+1].y); } // change pens pDC->SelectObject(&BluePen); // start drawing non animated tracks, but need to check for a // valid starting postion int start=pDoc->m_nBugPosition; if(start<0) start=0; for(i=start; i < N-2; i++) { pDC->MoveTo(pDoc->m_BugDataArr ay[i].x,)pDoc->m_BugDataArray[ i].y); pDC->LineTo(pDoc->m_BugDataArr ay[i+1].x,pDoc->m_BugDataArray [i+1].y); } // deselect pens and delete them pDC->SelectObject(pOldPen); RedPen.DeleteObject(); BluePen.DeleteObject(); // move to next position or quit animating if(pDoc->m_nBugPosition!=-1) pDoc->m_nBugPosition++; if(pDoc->m_nBugPosition>=N) { pDoc->m_nBugPosition=-1; // stop timer 1 KillTimer(1); // redraw and erase so all lines are in initial state (blue) InvalidateRect(NULL); } }
Ctrl-F5 the program to build and run it. Fix any bugs and you are done! Of course many improvements can be made, like scaling and centering the path to better fit the view, printing of the path, etc... but I think you have enough to go on. Good luck! (and don't be afraid of that F1 key)
- sandip's blog
- Login or register to post comments