Visual C++/MFC Tutorial - Lesson 7: Data Viewer
Lesson 7: 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.
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 <afxtempl.h>
and then in a public area of your class declare a CArray
template
class as follows:
CArray <SBugData, SBugData> m_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_BugDataArray[i].x, pDoc->m_BugDataArray[i].y); pDC->LineTo(pDoc->m_BugDataArray[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(text); }
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(strStatus); m_BugDataArray.Add(data); } // tell the user the total number of points strStatus.Format("Loaded %d points.", m_BugDataArray.GetSize()); pMain->ChangeStatusText(strStatus); } }
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_BugDataArray[i].x,)pDoc->m_BugDataArray[i].y); pDC->LineTo(pDoc->m_BugDataArray[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_BugDataArray[i].x,)pDoc->m_BugDataArray[i].y); pDC->LineTo(pDoc->m_BugDataArray[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)
Comments
Change static text color and size
Posted by Legacy on 11/15/2003 12:00amOriginally posted by: Frank
Hi,
ReplyI have a static text on the dialog. Can you show me how to change the text size and color?
Greatly appreciate your help.
Frank
7
Posted by Legacy on 11/13/2003 12:00amOriginally posted by: KRISHNADEVAN
Database Pgmm ???
Posted by Legacy on 11/03/2003 12:00amOriginally posted by: Shailesh
Good intro but give database programming also.
Reply
Changing a text color
Posted by Legacy on 10/28/2003 12:00amOriginally posted by: Stella Silina
Hello,
I just have a question.
I am working with Visual Studio 6.0
I have a static text in my Dialog Window.
I need to change its color.
Can you give me any suggestions on how to do it?
Thank you very much in advance.
Sincerely,
ReplyStella Silina.
Excellent tutorial
Posted by Legacy on 10/17/2003 12:00amOriginally posted by: KKamenov
It is a truly difficult task to summarize into a short and clear tutorial something as complex as MFC , but the author did an exellent job.
ReplyVery helpfull tutorial.
Exellent examples.
Really great tutorial
Posted by Legacy on 08/16/2003 12:00amOriginally posted by: hub
Hi,
I must say that Your tutorual is just what I was looking for! It covers some stuff that was either skipped in books (one book to be precise...) or explained pretty confusing (like timers in the manuals).
It is really great for a beggining and really easy to understand...
MoveTo and LineTo Functions in Lesson 7
Posted by Legacy on 06/18/2003 12:00amOriginally posted by: Sathish
Hi,
ReplyI was trying to run the program after typing your code. I am getting errors saying that MoveTo and LineTo are not members of the class CBugTracksDoc.
How do I add those functions. They are also not listed in the popup box when I type pDoc->
Please help me in this regard.
Thanks,
Sathish.
Help on Multi Frame display using SDI
Posted by Legacy on 06/06/2003 12:00amOriginally posted by: Chandra Mohan
Hi All,
Thanks for having a look.
I want to have a SDI window, but on clicking a button next, should be able to display some other frame over this.
(Similar to installation screens). Its an SDI Window and not dialog. Your suggestions will be highly useful.
Regards
ReplyChandra
Cheers.
Posted by Legacy on 02/18/2003 12:00amOriginally posted by: Shazrul
Thank you.
ReplyCheers.
Posted by Legacy on 02/18/2003 12:00amOriginally posted by: Shazrul
Thank you.
ReplyLoading, Please Wait ...