In Lab 3, you learned how to implement a very simple OnDraw() function for the STUpload application, which outputs the data records held in the application document object as lines of text. The application displays all the records that are stored in the document—currently a hard-coded set of records representing data from three different stock funds.
In Lab 5, you are going to enhance the application in two ways. First, you are going to implement the Select Fund dialog box, a modeless dialog box that allows the user to select from a list of funds currently stored in the application document object. Then you will create a new CSTUploadView::OnDraw() function to display data for the currently selected fund in a graph format.
To create the Select Fund dialog box, you will use the dialog template IDD_ FUNDDIALOG and the class CFundDialog that you created for Lab 4. At this point, the dialog box design is not complete. The final version will not have OK and Cancel buttons because you are going to allow the user to show and hide the dialog box using only the Select Fund command option on the View menu (or by using the toolbar button). However, you will need to overload the OnOK() and OnCancel() handler functions. Because IDOK and IDCANCEL messages will be generated when the user presses the ENTER or ESC keys, you will need to create versions of the handler functions for the Select Fund dialog class that do nothing; otherwise, the base class (CDialog) versions will be called. Remember that the base class versions call the EndDialog() function to dismiss the dialog box.
It is much easier to add handlers for the OK and Cancel messages when the buttons are still defined as part of the dialog box, so you will create OnOK() and OnCancel() handlers at this point.
CDialog::OnOK(); |
Figure 5.20 The Select Fund dialog box
Now you will write code to control the behavior of the Select Fund dialog box. You will create a single CFundDialog object early on in the application, as the m_wndFundDialog member of the application main frame window object CMainFrame. The dialog window will be a child window of the application main window, and it will be shown or hidden according to the status of m_bFundsVisible, a Boolean member variable of the CMainFrame class. The status of m_bFundsVisible will be set by menu and toolbar commands.
#include "FundDialog.h" |
m_bFundsVisible = FALSE; |
(This code can be found in CH5_01.cpp, installed from the companion CD.)
BOOL AreFundsVisible() {return m_bFundsVisible;} void SetFundsVisible(BOOL bSet) { m_bFundsVisible = bSet; if(bSet) m_wndFundDialog.ShowWindow(SW_SHOW); else m_wndFundDialog.ShowWindow(SW_HIDE); } |
// Create the fund dialog window m_wndFundDialog.Create(IDD_FUNDDIALOG); |
Remember that the dialog box does not have the WS_VISIBLE style property, so it will need to be displayed explicitly with a call to the CWnd::ShowWindow() function.
Hiding and showing the dialog box is going to be controlled by the handler for the Selection Fund command on the View menu. In the following exercises, you will create a command handler and a user interface update command handler for the ID_VIEW_FUNDSELECTION command ID.
SetFundsVisible(m_bFundsVisible ? FALSE : TRUE); |
(This code can be found in CH5_02.cpp, installed from the companion CD.)
// Enable the View Funds Selection dialog if // the document CStockDataList is not empty. // If enabled, then toggle button state checked/unchecked // according to whether the window is displayed or hidden BOOL bEnable = FALSE; CSTUploadDoc * pDoc = dynamic_cast<CSTUploadDoc *>(GetActiveDocument()); if(pDoc) bEnable = pDoc->GetDocList().GetCount() ? TRUE : FALSE; pCmdUI->Enable(bEnable); if(bEnable) pCmdUI->SetCheck(m_bFundsVisible ? 1 : 0); |
#include "STUploadDoc.h" |
Now that you have displayed the Select Fund dialog box, you will write a function to load the names of the funds into the list box control. Because the list of fund records held in the CSTUploadDoc::m_DocList object will be created in fund sort order, it is not a difficult matter to iterate across the list and extract a unique list of fund names.
Before you can write this function, you will have to create a variable with the MFC type CListBox.
You are now going to add the UpdateFundList() member function to the CMainFrame class to perform the task of loading the fund name items into the list box. The function will take two parameters—a reference to a CStockDataList, which will be the source of the fund names; and a string to specify the fund name that should be selected initially. If the second parameter contains an empty string (the default value), or the provided string is not found in the list box, no item is selected.
#include "StockDataList.h" |
void UpdateFundList(const CStockDataList & pList, CString strCurrentFund = ""); |
(This code can be found in CH5_03.cpp, installed from the companion CD.)
// Function to add one entry per fund to fund view list box. // CStockDataLists are sorted by fund name so this is easy. CListBox *pListBox = &m_wndFundDialog.m_listBox; // Empty current contents of list box pListBox->ResetContent(); CString strLastFund; POSITION pos = pList.GetHeadPosition(); while(pos) { CStockData sd = pList.GetNext(pos); CString strFund = sd.GetFund(); if(strFund != strLastFund) pListBox->AddString(strFund); strLastFund = strFund; } // Set list box selection to strCurrentFund parameter. // No selection if parameter empty or not found. int iPos = pListBox->FindStringExact(-1, strCurrentFund); pListBox->SetCurSel(iPos); |
Look through the code and make sure that you understand how the function achieves its objectives.
The UpdateFundList() function will be called from the LoadData() function. Eventually the LoadData() function will handle the loading of data records from a text file. For now, you will create a temporary version that simply adds some hard-coded records.
BOOL LoadData(CStdioFile & infile) |
m_DocList.AddTail(CStockData(_T("ARSC"), COleDateTime(1999, 4, 1, 0, 0, 0), 22.33)); // ... more records added here m_DocList.AddTail(CStockData(_T("COMP"), COleDateTime(1999, 4, 5, 0, 0, 0), 19.77)); // Update main window UpdateAllViews(NULL); // Update Fund Selection dialog box CMainFrame * pWnd = dynamic_cast<CMainFrame *> (AfxGetMainWnd()); if(pWnd) { pWnd->UpdateFundList(m_DocList); // Show fund window after loading new funds pWnd->SetFundsVisible(TRUE); } return TRUE; |
#include "MainFrm.h" |
CSTUploadDoc::CSTUploadDoc() { } |
if(nID == IDOK) { CStdioFile aFile; LoadData(aFile); } |
You can now build the STUpload application. Choose Import from the Data menu. Using the Open dialog box, open the Ch5Test.dat file from the ..\Chapter 5\Data folder. Records should be displayed for the ARSC, BBIC and COMP funds, and the Select Fund dialog box should display these three funds.
The purpose of the Select Fund dialog box is to allow the user to limit the display of fund price data so that records for only the currently selected fund are displayed. You have already filled the list box with options—you now have to act upon the user's selection.
To start, you will add the m_strCurrentFund variable to record the fund name currently selected by the user. In the next chapter, you will be making this variable persistent so that you can save the current user selection as part of the document data. Thus the m_strCurrentFund variable will be a member of the CSTUploadDoc class.
To ensure that this variable always represents the current selection of the Select Fund dialog box, you will handle notification messages from the list box control. The list box sends a LBN_SELCHANGE message to its parent window (the CFundDialog object) every time the selection changes. You can use ClassWizard to provide a handler for this message.
You will also change the CSTUploadView::OnDraw() function so that it refers to the "currently selected fund" variable held by the document object to ensure that it displays records that pertain to the currently selected fund only.
Right-click the CDocument icon in ClassView and add a protected member variable of type CString named m_strCurrentFund.
m_strCurrentFund = ""; |
Add the following lines of code to the public section of the CDocument class declaration:
CString GetCurrentFund () {return m_strCurrentFund;} void SetCurrentFund (CString strSet){m_strCurrentFund= strSet;} |
(This code can be found in CH5_05.cpp, installed from the companion CD.)
CMainFrame * pWnd = dynamic_cast<CMainFrame *> (AfxGetMainWnd()); ASSERT_VALID(pWnd); CSTUploadDoc * pDoc = dynamic_cast<CSTUploadDoc *>(pWnd->GetActiveDocument()); ASSERT_VALID(pDoc); CString strCurFund; int sel = m_listBox.GetCurSel(); if(sel == LB_ERR) sel = 0; m_listBox.GetText(sel, strCurFund); pDoc->SetCurrentFund(strCurFund); pDoc->UpdateAllViews(NULL); |
#include "MainFrm.h" #include "STUploadDoc.h" |
if(sd.GetFund() != pDoc->GetCurrentFund()) continue; |
The entire loop section should now look as follows:
while(pos) { CStockData sd = pData.GetNext(pos); if(sd.GetFund() != pDoc->GetCurrentFund())continue; pDC->TextOut(10, yPos, sd.GetAsString()); yPos += nTextHeight; } |
The Select Fund dialog box is a key element of the STUpload application user interface. It will be constantly in use to switch from fund to fund as the operator checks through the data held on file. In its current implementation, however, it is easily hidden if the user clicks on the main application window—to access the main menu, for example. The Select Fund dialog box is so small that it is inconvenient to retrieve once it is hidden behind the larger window.
The solution to this problem is to make the Select Fund dialog box a topmost window—that is, a window that is always on top of other windows in the application. A topmost window will appear even if it does not have input focus.
A topmost window has the style WS_EX_TOPMOST. In an MFC application, you can set this by calling the CWnd::SetWindowPos() function with the address of the wndTopMost constant as the first parameter.
m_wndFundDialog.Create(IDD_FUNDDIALOG); |
and before the return statement, add the line shown below.
m_wndFundDialog.SetWindowPos(&wndTopMost, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE); |
This is all you need to do to make the Select Fund dialog box stay on top of the application main window. Unfortunately, it has the undesirable side effect of making the dialog box stay on top of all other application windows—even when the STUpload application is not active. If you build and run the application and test its behavior at this point, you will see that the Select Fund dialog box remains visible even when the STUpload application is minimized.
To solve this problem, you need to hide the dialog box (if it is visible) at the point where the application as a whole loses focus, and then re-display it when the user switches focus back to the application. To do this, you need to provide a handler for the WM_ACTIVATEAPP message, which is called when the user switches between applications. You will use ClassWizard to create a handler that is an overload of the CWnd::OnActivateApp() method. This function is called by the framework with a Boolean parameter that indicates whether the application is being activated or deactivated.
(This code can be found in CH5_15.cpp, installed from the companion CD.)
if(bActive) { if(AreFundsVisible()) m_wndFundDialog.ShowWindow(SW_SHOW); } else { if(AreFundsVisible()) m_wndFundDialog.ShowWindow(SW_HIDE); } |
You are now ready to implement the graphical display of price data for the currently selected fund. The first task is to determine the document size so that the scroll bars provided by CScrollView will appear correctly. In the STUpload application, although the size of the application data changes as text files are loaded into the document, the size of the display output remains constant. Only one graph is displayed at any time. You will fix the size of the graph to the size of a standard laser printer page in landscape orientation—that is, 11 inches wide by 8.5 inches tall.
sizeTotal.cx = sizeTotal.cy = 100; SetScrollSizes(MM_TEXT, sizeTotal); |
so that they appear as follows:
sizeTotal.cx = 1100; sizeTotal.cy = 850; SetScrollSizes(MM_LOENGLISH, sizeTotal); |
You will now replace the CView::OnDraw() function with a provided version. This function collects the data currently held on file for a particular fund into a temporary array. It uses the data to calculate a suitable scale for the dates (x-axis) and the prices (y-axis). The data is presented as a line graph, which should enable the operator to spot any erratic data easily, with the exact values displayed at the graph points. Look through the code to see how the MFC drawing tool classes and the GDI drawing functions are used to display output in a device context.
(This code can be found in CH5_07.cpp, installed from the companion CD.)
void CSTUploadView::OnDraw(CDC* pDC) { CSTUploadDoc* pDoc = GetDocument(); ASSERT_VALID(pDoc); // Save the current state of the device context int nDC = pDC->SaveDC(); const CStockDataList & pData = pDoc->GetDocList(); // Make a small array containing the // records for the current fund. // We use an array to take advantage of indexed access. CArray<CStockData, CStockData &> arrFundData; POSITION pos = pData.GetHeadPosition(); while(pos) { CStockData sd = pData.GetNext(pos); if(sd.GetFund() == pDoc->GetCurrentFund()) arrFundData.Add(sd); } int nPrices = arrFundData.GetSize(); if(nPrices == 0) return; // Some constant sizes (in device units) const int AXIS_DIVIDER_LENGTH = 6; const int AXIS_FONT_HEIGHT = 24; const int HEADING_FONT_HEIGHT = 36; // Create font for axis labels CFont AxisFont; if(AxisFont.CreateFont(AXIS_FONT_HEIGHT, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, FF_ROMAN, 0)) pDC->SelectObject(&AxisFont); else { AfxMessageBox("Unable to create Axis font"); return; } CPen AxisPen; if(AxisPen.CreatePen(PS_SOLID, 1, RGB(0,0,0))) pDC->SelectObject(&AxisPen); else { AfxMessageBox("Unable to create Axis Pen"); return; } // Array to graph coordinates as we go CArray<CPoint, CPoint&> CoordArray; for(int i = 0; i < nPrices; i++) CoordArray.Add(CPoint(0, 0)); // Set viewport origin to bottom left corner of window CPoint ptBottomLeft(0, -850); pDC->LPtoDP(&ptBottomLeft); pDC->SetViewportOrg(ptBottomLeft); // Base coordinates for axes const CPoint ORIGIN(100, 100); const CPoint Y_EXTENT(ORIGIN.x, ORIGIN.y + 650); const CPoint X_EXTENT(ORIGIN.x + 900, ORIGIN.y); // Draw axes pDC->MoveTo(Y_EXTENT); pDC->LineTo(ORIGIN); pDC->LineTo(X_EXTENT); int nLabelPos = Y_EXTENT.y + ((ORIGIN.y - Y_EXTENT.y) / 2); pDC->TextOut(ORIGIN.x - 50, nLabelPos, '$'); // Divide x-axis into number of prices held in the file int nXIncrement = (X_EXTENT.x - ORIGIN.x) / nPrices; double nMaxPrice = 0; double nMinPrice = 0; for(i = 0; i < nPrices; i++) { int xPoint = (ORIGIN.x + (i * nXIncrement)); CoordArray[i].x = xPoint; pDC->MoveTo(xPoint, ORIGIN.y); pDC->LineTo(xPoint, ORIGIN.y + AXIS_DIVIDER_LENGTH); COleDateTime aDate = arrFundData[i].GetDate(); double aPrice = arrFundData[i].GetPrice(); nMaxPrice = max(nMaxPrice, aPrice); nMinPrice = nMinPrice == 0 ? nMaxPrice : min(nMinPrice, aPrice); CString strDate = aDate.Format("%m/%d/%y"); if(i == 0 || i == (nPrices-1)) pDC->TextOut(xPoint-2, ORIGIN.y - AXIS_FONT_HEIGHT / 2, strDate); else { CString strDay = strDate.Mid( strDate.Find('/') + 1); strDay = strDay.Left(strDay.Find('/')); pDC->TextOut(xPoint-6, ORIGIN.y - AXIS_FONT_HEIGHT / 2, strDay); } } // Divide y-axis into suitable scale based on // the difference between max and min prices on file nMaxPrice += 2.0; nMinPrice -= 1.0; int iScale = int(nMaxPrice) - int(nMinPrice); int nYIncrement = (ORIGIN.y - Y_EXTENT.y) / iScale; for(i = 0; i < iScale; i++) { int yPoint = (ORIGIN.y - (i * nYIncrement)); pDC->MoveTo(ORIGIN.x, yPoint); pDC->LineTo(ORIGIN.x - AXIS_DIVIDER_LENGTH, yPoint); int iCurrentPrice = int(nMinPrice) + i; for(int j = 0; j < nPrices; j++) { double aPrice = arrFundData[j].GetPrice(); if(aPrice >= double(iCurrentPrice) && aPrice < double(iCurrentPrice) + 1.0) { double dFraction = aPrice - double(iCurrentPrice); CoordArray[j].y = yPoint - int(dFraction * double(nYIncrement)); } } CString strPrice; strPrice.Format("%d", iCurrentPrice); int nTextSize = pDC->GetTextExtent(strPrice).cx; nTextSize += 10; pDC->TextOut(ORIGIN.x - nTextSize, yPoint+12, strPrice); } // Graph figures stored in CoordArray CPen GraphPen; if(GraphPen.CreatePen(PS_SOLID, 1, RGB(255,0,0))) // Red pen { pDC->SelectObject(&GraphPen); } else { AfxMessageBox("Unable to create Graph Pen"); return; } // Draw Graph // Label graph points with price value (in blue) COLORREF crOldText = pDC->SetTextColor(RGB(0,0,255)); pDC->MoveTo(CoordArray[0]); for(i = 0; i <nPrices; i++) { pDC->LineTo(CoordArray[i]); CPoint TextPoint; if((i+1) <nPrices) { if(CoordArray[i + 1].y >= CoordArray[i].y) TextPoint = CoordArray[i] + CPoint(5, 0); else TextPoint = CoordArray[i] + CPoint(5, AXIS_FONT_HEIGHT); } else TextPoint = CoordArray[i] + CPoint(5, 0); CString strPrice; strPrice.Format("%.2f", arrFundData[i].GetPrice()); pDC->TextOut(TextPoint.x, TextPoint.y, strPrice); } pDC->SetTextColor(crOldText); // Create heading CFont HeadingFont; if(HeadingFont. CreateFont(HEADING_FONT_HEIGHT, 0, 0, 0, FW_BOLD, 1, 0, 0, 0, 0, 0, 0, FF_ROMAN, 0)); pDC->SelectObject(&HeadingFont); else { AfxMessageBox("Unable to create Heading Font"); return; } CString strHeading = pDoc->GetCurrentFund(); strHeading += " - Closing Prices "; COleDateTime aDate = arrFundData[0].GetDate(); strHeading += aDate.Format("%m/%d/%y"); strHeading += " to "; aDate = arrFundData[nPrices - 1].GetDate(); strHeading += aDate.Format("%m/%d/%y"); CSize sizeText = pDC->GetTextExtent(strHeading); pDC->TextOut(X_EXTENT.x - sizeText.cx, Y_EXTENT.y + sizeText.cy, strHeading); // Restore the original device context pDC->RestoreDC(nDC); } |
Figure 5.21 The STUpload application