Survey
* Your assessment is very important for improving the workof artificial intelligence, which forms the content of this project
* Your assessment is very important for improving the workof artificial intelligence, which forms the content of this project
Entity–attribute–value model wikipedia , lookup
Open Database Connectivity wikipedia , lookup
Functional Database Model wikipedia , lookup
Clusterpoint wikipedia , lookup
Relational model wikipedia , lookup
Microsoft Jet Database Engine wikipedia , lookup
Object-relational impedance mismatch wikipedia , lookup
Windows MFC Programming II 411 Chapter 8 — Database Report Printing Often business report applications must access some external database in order to acquire the necessary data. In this lengthy chapter, the database classes are explored along with printing reports and putting user defined data onto the clipboard. Over the years, the programming interfaces to databases keeps changing. Currently, the MFC supports three different systems. ODBC, Open DataBase Connectivity, provides a uniform method of accessing many types of databases, including SQL as well as Access databases. Their older class libraries, the DAO (Database Access Object) classes are used to access a Microsoft Access database of tables and queries. While the Application Wizard no longer generates such classes for us and the compiler states these are now old versions, Microsoft still claims that these classes provide the fastest access for an Access database, using the Jet engine directly. The third supported type of database system is the OLE DB. The ODBC and DAO classes and methods are very similar in nature and coding. While the DAO classes are “obsolete,” they still provide the best performance against an Access database. Since I use Access databases in my game programming, I will cover them here as well, for a game needs the best access time. This is not a primer on database SQL and its features. One could use SQL statements to join the sample tables into one query result for faster access and formation of the reports, I will use other means to illustrate other actions that can be performed with databases. Just realize that using joins would make these sample programs easier to implement. The External Access Database The place to begin is with the external Access database that the application is using. I recommend that beginners always start with a pre-built database, even if you have to place dummy records in it. Why? Then you can see quickly if your classes and program is working. In the Pgm08a folder is the database known as Acme.mdb which contains three tables and two queries. Although I have kept the database both small and relatively simple, quite a number of actions can be done with it. In Figure 8.1, I have shown the Access opening window with the three tables available and the two query results tables. Acme sells three categories of products: coffee and tea, beer, and sodas. The Categories Table provides the name for each category number; our usage of this table will be to look up the name for a specific category number. That is, we must use a seek for a specific record. Each category has a number of products within it. The Items Table provides the product name for each item number along with its category and unit cost. Again, our usage is to seek for a record with a specific item number to retrieve its category, name, and cost. The Sales Table represents the transactions, providing the item number Windows MFC Programming II 412 and the quantity sold in that transaction. This is the table that our application is to directly scroll and edit, including adding new transactions, updating existing ones, and deleting specific records. It is from this table, that the final printed sales report is to be generated, accumulating the total sales by category and item within that category. Figure 8.1 Microsoft Access with the Acme Sales Database Opened (Acme.mdb) Assume that the category numbers begin with zero and increase consecutively. Likewise, the item numbers. For now, we can ignore the exact data types of the field contents. The database classes extract that information automatically. Each field becomes a public member in our C++ classes. When you examine the five ODBC or DAO classes, you can see how each field is defined. To get a better feel for what the application is to do, examine Figures 8.2 and 8.3 which show the first page of the final sales report in print preview mode and the main window of the Windows MFC Programming II 413 application, Pgm08a. Figure 8.2 The Sales Report — Page 1 In the report, the total sales of each item in one category is displayed per page. On the left 1/3 of the page is the columnar view, while the bar chart occupies the right 2/3 of the page. (If you are familiar with Access, the report should look a bit familiar. I modeled it from those generated by Access.) When it is time to print the report, the application must calculate each transaction’s sales and accumulate them. Only the totals are printed. Figure 8.3 shows the formview with the two edit controls that display the current record’s data. The view allows for adding new records, updating existing records, and deleting records. Deleted records will go to the clipboard and can be pasted back into the database as an Undo menu choice. The figure also shows what appears when the Quick view button is pressed, showing how the first page of the report might look. Yes, this application illustrates quite a number of actions, not the least is dynamically allocating two-dimensional tables to hold the results and transferring data to and from the Clipboard using our own user-defined data. Windows MFC Programming II Figure 8.3 The View Window after the “Q” Quick View of the Report Is Pressed First, let’s see how the application can be created using the preferred MFC classes for ODBC. 414 Windows MFC Programming II 415 The ODCB MFC Classes These classes work with the other application framework classes to give easy access to a wide variety of databases for which Open Database Connectivity (ODBC) drivers are available. Programs will usually have several class instances. The document holds the CDatabase instance, which encapsulates your connection to a data source, Acme.mdb in this case. There is only one of these instances per database. Each table or query that you wish to access will have a CRecordset class, which encapsulates a set of records that are selected from a data source. The recordsets allow scrolling from record to record, updating records, by which is meant adding, editing, and deleting, using a filter to select specific records, sorting the results, and allowing for parameterization of the selection with information obtained at run time. We will use this parameterization to select only those records which match a specific sales number id within other tables. In order to show the data onscreen, often a form view is used, which looks much like a dialog with various controls, yet it is a CView based class, called a CRecordView. It is a form view that is directly connected to a recordset, showing its data in the view’s controls. The dialog data exchange (DDX) mechanism exchanges data between the recordset and the controls of the record view. The form view is based on a dialog template resource. Record views also support moving from record to record in the recordset, updating records, and closing the associated recordset when the record view closes. A similar method handles transferring the data from the recordset to and from the actual database itself, the CFieldExchange. CFieldExchange supplies information to support record field exchange (RFX macros similar to DDX), which is an exchange of data between the field data members and data members of a recordset object and the corresponding table columns on the database source. These classes throw a CDBException whenever there is an error in the data access processing. Windows MFC Programming II 416 Using the Application Wizard to Build a Beginning ODBC Shell The starting point is to let the Application Wizard build a shell app for us. I made a folder, test8, and copied the acme.mdb file into that folder. Now, I chose MFC single document, doc-view, not using the UNICODE libraries. See if you can duplicate the following steps, after making a test8 subfolder and copying the mdb file. Follow the screen shots, step by step. Figure 8-4 Application Wizard Database Settings, Press Data Source Now Next, chose Database Support, checking ODBC, bind all columns, using dynaset, which will allow updating. Snapshot gives you a static view of the tables. Next, press the datasource button to tie it to Acme.mdb. Figure 8.5 shows the initial Choose DSN Dialog. Press the New button. Windows MFC Programming II 417 Figure 8.5 The Choose DSN Dialog Starting Point, Press New In the dialog I pressed New Database button beside the DSN edit control. In the next dialog, I chose Microsoft Access and then the Browse button to go find the test8 folder and the Acme.mdb file. You will have to change the combobox to all files to see the mdb file to select it. Figure 8.6 Chose the Access Driver Windows MFC Programming II 418 Figure 8.7 The Access Driver Selected, Press Finish Figure 8.8 Creating a New Data Source, Acme.mdb With the DSN setup, now click Next to select which access database you desire, shown in Figure 8.9 below. Press the Select button. Windows MFC Programming II Figure 8.9 Setting Up the Access Table, Press Select Figure 8.10 Select Acme, Press OK Choose the Acme.mdb file in the test8 subfolder. Click OK. Finally, we need to chose which table to use. Select Sales, as shown in Figure 8.11 below. 419 Windows MFC Programming II Figure 8.11 Selecting the File Data Source Figure 8.12 Select the Sales Table 420 Windows MFC Programming II 421 At last, finish making your app choices and click finish. The wizard builds a shell application. Compile and run it. There will be one compile error, warning about a security problem. Comment out that #error line and rebuild. You should see a view shown in Figure 8-13 below. Notice the framework has added four buttons to handle the movement to the next record, previous record, last record, and first record. Figure 8-13 The View Now let’s add two edit controls so we can see the record’s fields. Open the resource editor on the dialog view template, remove the static text field and add two edit controls. Change their properties to use item number and quantity as part of their control IDC names. Then, add to member variables for them, by right clicking on the control and choosing Add Variable. Again, call one some variation of item number and the other quantity. Now open the recordset file, here called test8set.h. Find the two names of the member variables which hold the current row of values in the table. Their names are closely similar to those of the actual table column names: int m_ItemNumber and long m_QuantitySold. Copy these names to the clipboard and open the view cpp file. Scroll down to the DoDataExchange function and temporarily paste the two names on a separate line. Notice that the wizard has given you two commented lines for the sample field text transfer from the recordset. Change them to read something like the following, using your control IDC names. DDX_FieldText(pDX, IDC_EDITSALES, m_pSet->m_ItemNumber, m_pSet); DDX_FieldText(pDX, IDC_EDITQUANTITY, m_pSet->m_QuantitySold, m_pSet); Now rebuild and run the application. You should see the first record appearing in the controls, shown in Figure 8-14. Figure 8-14 View’s First Record Windows MFC Programming II 422 Experiment with the four recordset positioning buttons. These default movement base class functions have some behavior that may not be desirable in your applications. Specifically, if you add a new record, you must press one of the move buttons to finalize it as well as call the Update() function to store it, otherwise the add is lost. If you go into Update() mode on the current record, moving away cancels the update unless you call the appropriate function. In my application, I will be overriding the basic behavior to gain greater control over the operation, making it easier for the user to handle adds, updates, and deletions. Since we have five tables and queries, we will need five recordset classes. When you choose Add Class, choose ODBC Container, as shown in Figure 8-14 below. Figure 8-15 Adding Additional ODBC Recordset Classes You will once more have to go through that extensive set of dialogs, adding the Access Driver, choosing Acme.mdb.dsn, and eventually Acme.mdb. Finally, you will get to chose which table or query to which you want to bind the class. Windows MFC Programming II 423 The ODBC Classes of Pgm08a The cpp implementation files had the full path to the files in them, I removed these huge strings, shortening them to default to our project folder. Listing for File: ODBCCategories.h — Pgm08a #pragma once class ODBCCategories : public CRecordset { public: ODBCCategories (CDatabase* pDatabase = NULL); DECLARE_DYNAMIC(ODBCCategories) BYTE Cstring m_CategoryNumber; // category number m_Category; // category name public: virtual CString GetDefaultConnect(); // Default connection string virtual CString GetDefaultSQL(); // default SQL for Recordset virtual void DoFieldExchange(CFieldExchange* pFX); // RFX support ... This file retrieves the different categories from the Categories table. Listing for File: ODBCCategories.cpp — Pgm08a #include "stdafx.h" #include "ODBCCategories.h" IMPLEMENT_DYNAMIC(ODBCCategories, CRecordset) /***************************************************************************/ /* */ /* ODBCCategories: access the Category Table */ /* */ /***************************************************************************/ ODBCCategories::ODBCCategories(CDatabase* pdb) : CRecordset(pdb) { m_CategoryNumber = 0; m_Category = L""; m_nFields = 2; m_nDefaultType = CRecordset::snapshot; // dynaset; } CString ODBCCategories::GetDefaultConnect() { return _T("DBQ=Acme.mdb;DefaultDir=;Driver={Microsoft Access Driver (*.mdb)};DriverId=281;FIL=MS Access;FILEDSN=Acme.mdb.dsn;MaxBufferSize=2048;MaxScanRows=8;PageTimeout=5;Saf eTransactions=0;Threads=3;UID=admin;UserCommitSync=Yes;"); } CString ODBCCategories::GetDefaultSQL() { return _T("[Categories]"); Windows MFC Programming II 424 } void ODBCCategories::DoFieldExchange(CFieldExchange* pFX) { pFX->SetFieldType(CFieldExchange::outputColumn); RFX_Byte(pFX, _T("[Category Number]"), m_CategoryNumber); RFX_Text(pFX, _T("[Category]"), m_Category); } Notice How the fields are automatically exchanged with the recordset variables, highlighted in bold. Listing for File: ODBCCategoryCount.h — Pgm08a This class holds the query which counts the number of categories in the Categories table. #pragma once class ODBCCategoryCount : public CRecordset { public: ODBCCategoryCount (CDatabase* pDatabase = NULL); DECLARE_DYNAMIC(ODBCCategoryCount) long m_CountOfCategoryNumber; // Number of categories query public: virtual CString GetDefaultConnect(); // Default connection string virtual CString GetDefaultSQL(); // default SQL for Recordset virtual void DoFieldExchange(CFieldExchange* pFX); // RFX support #ifdef _DEBUG virtual void AssertValid() const; virtual void Dump(CDumpContext& dc) const; #endif }; Listing for File: ODBCCategoryCount.cpp — Pgm08a #include "stdafx.h" #include "ODBCCategoryCount.h" IMPLEMENT_DYNAMIC(ODBCCategoryCount, CRecordset) /***************************************************************************/ /* */ /* ODBCCategoryCount: access the query results from Category Count Table */ /* */ /***************************************************************************/ ODBCCategoryCount::ODBCCategoryCount(CDatabase* pdb) : CRecordset(pdb) { m_CountOfCategoryNumber = 0; m_nFields = 1; m_nDefaultType = dynaset; } CString ODBCCategoryCount::GetDefaultConnect() { return _T("DBQ=Acme.mdb;DefaultDir=;Driver={Microsoft Access Driver Windows MFC Programming II 425 (*.mdb)};DriverId=281;FIL=MS Access;FILEDSN=Acme.mdb.dsn;MaxBufferSize=2048;MaxScanRows=8;PageTimeout=5;Saf eTransactions=0;Threads=3;UID=admin;UserCommitSync=Yes;"); } CString ODBCCategoryCount::GetDefaultSQL() { return _T("[Category Count]"); } void ODBCCategoryCount::DoFieldExchange(CFieldExchange* pFX) { pFX->SetFieldType(CFieldExchange::outputColumn); RFX_Long(pFX, _T("[CountOfCategory Number]"), m_CountOfCategoryNumber); } Listing for File: ODBCCountItemsPerCategory.h — Pgm08a This class holds the query results of counting the number of items in each category. #pragma once class ODBCCountItemsPerCategory : public CRecordset { public: ODBCCountItemsPerCategory (CDatabase* pDatabase = NULL); DECLARE_DYNAMIC(ODBCCountItemsPerCategory) BYTE long m_CategoryNumber; // category number m_CountOfItemNumber; // number of items in this category public: virtual CString GetDefaultConnect(); // Default connection string virtual CString GetDefaultSQL(); // default SQL for Recordset virtual void DoFieldExchange(CFieldExchange* pFX); // RFX support ... Listing for File: ODBCCountItemsPerCategory.cpp — Pgm08a #include "stdafx.h" #include "ODBCCountItemsPerCategory.h" /***************************************************************************/ /* */ /* ODBCCountItemsPerCategory: access the query results from Count Items Per*/ /* Category */ /* */ /***************************************************************************/ IMPLEMENT_DYNAMIC(ODBCCountItemsPerCategory, CRecordset) ODBCCountItemsPerCategory::ODBCCountItemsPerCategory (CDatabase* pdb) : CRecordset(pdb) { m_CategoryNumber = 0; m_CountOfItemNumber = 0; m_nFields = 2; m_nDefaultType = dynaset; } Windows MFC Programming II 426 CString ODBCCountItemsPerCategory::GetDefaultConnect() { return _T("DBQ=Acme.mdb;DefaultDir=;Driver={Microsoft Access Driver (*.mdb)};DriverId=281;FIL=MS Access;FILEDSN=Acme.mdb.dsn;MaxBufferSize=2048;MaxScanRows=8;PageTimeout=5;Saf eTransactions=0;Threads=3;UID=admin;UserCommitSync=Yes;"); } CString ODBCCountItemsPerCategory::GetDefaultSQL() { return _T("[Count Items Per Category]"); } void ODBCCountItemsPerCategory::DoFieldExchange(CFieldExchange* pFX) { pFX->SetFieldType(CFieldExchange::outputColumn); RFX_Byte(pFX, _T("[Category Number]"), m_CategoryNumber); RFX_Long(pFX, _T("[CountOfItem Number]"), m_CountOfItemNumber); } ... Listing for File: ODBCItems.h — Pgm08a This class holds the records from the Items table. It is the basic recordset for the whole table. Ths ODBCGetItems class also loads the same table, but is special; it retrieves a recordset with a matching ItemNumber. #pragma once class ODBCItems : public CRecordset { public: ODBCItems(CDatabase* pDatabase = NULL); DECLARE_DYNAMIC(ODBCItems) int Cstring BYTE double m_ItemNumber; m_ItemName; m_CategoryNumber; m_UnitCost; // // // // item number item name corresponding category number unit cost of this item public: virtual CString GetDefaultConnect(); // Default connection string virtual CString GetDefaultSQL(); // default SQL for Recordset virtual void DoFieldExchange(CFieldExchange* pFX); // RFX support ... Listing for File: ODBCItems.cpp — Pgm08a #include "stdafx.h" #include "ODBCItems.h" IMPLEMENT_DYNAMIC(ODBCItems, CRecordset) /***************************************************************************/ /* */ /* ODBCItems: Access the Items Table */ /* */ /***************************************************************************/ Windows MFC Programming II 427 ODBCItems::ODBCItems(CDatabase* pdb) : CRecordset(pdb) { m_ItemNumber = 0; m_ItemName = L""; m_CategoryNumber = 0; m_UnitCost = 0.0; m_nFields = 4; m_nDefaultType = dynaset; } CString ODBCItems::GetDefaultConnect() { return _T("DBQ=Acme.mdb;DefaultDir=;Driver={Microsoft Access Driver (*.mdb)};DriverId=281;FIL=MS Access;FILEDSN=Acme.mdb.dsn;MaxBufferSize=2048;MaxScanRows=8;PageTimeout=5;Saf eTransactions=0;Threads=3;UID=admin;UserCommitSync=Yes;"); } CString ODBCItems::GetDefaultSQL() { return _T("[Items]"); } void ODBCItems::DoFieldExchange(CFieldExchange* pFX) { pFX->SetFieldType(CFieldExchange::outputColumn); RFX_Int(pFX, _T("[Item Number]"), m_ItemNumber); RFX_Text(pFX, _T("[Item Name]"), m_ItemName); RFX_Byte(pFX, _T("[Category Number]"), m_CategoryNumber); RFX_Double(pFX, _T("[Unit Cost]"), m_UnitCost); } Listing for File: ODBCGetItem.h — Pgm08a As the report is being created, given the current item number, I need to look it up in the Items table to obtain its description and such. While one could write specific SQL commands, since I do not assume that the reader knows SQL, I am illustrating how to parameterize a recordset. Notice the slight changes in bold. We add a member variable that will be filled up at run time with various item numbers to look up in this table. A Requery() function call will reload the recordset with the matching record. #pragma once class ODBCGetItem : public CRecordset { public: ODBCGetItem (CDatabase* pDatabase = NULL); DECLARE_DYNAMIC(ODBCGetItem) int CString BYTE double CString m_ItemNumber; m_ItemName; m_CategoryNumber; m_UnitCost; m_FindItemParam; // filled by caller to be able to find one record public: virtual CString GetDefaultConnect(); // Default connection string virtual CString GetDefaultSQL(); // default SQL for Recordset virtual void DoFieldExchange(CFieldExchange* pFX); // RFX support Windows MFC Programming II 428 ... Listing for File: ODBCGetItem.cpp — Pgm08a To make it work, in the ctor, I set the number of parameters the query will be having, here one, the item number on which to match. #include "stdafx.h" #include "ODBCGetItem.h" IMPLEMENT_DYNAMIC(ODBCGetItem, CRecordset) ODBCGetItem::ODBCGetItem(CDatabase* pdb) : CRecordset(pdb) { m_ItemNumber = 0; m_ItemName = L""; m_CategoryNumber = 0; m_UnitCost = 0.0; m_nFields = 4; m_nDefaultType = dynaset; m_nParams = 1; } CString ODBCGetItem::GetDefaultConnect() { return _T("DBQ=Acme.mdb;DefaultDir=;Driver={Microsoft Access Driver (*.mdb)};DriverId=281;FIL=MS Access;FILEDSN=Acme.mdb.dsn;MaxBufferSize=2048;MaxScanRows=8;PageTimeout=5;Saf eTransactions=0;Threads=3;UID=admin;UserCommitSync=Yes;"); } CString ODBCGetItem::GetDefaultSQL() { return _T("[Items]"); } void ODBCGetItem::DoFieldExchange(CFieldExchange* pFX) { pFX->SetFieldType(CFieldExchange::param); RFX_Text (pFX, "Param", m_FindItemParam); pFX->SetFieldType(CFieldExchange::outputColumn); RFX_Int(pFX, _T("[Item Number]"), m_ItemNumber); RFX_Text(pFX, _T("[Item Name]"), m_ItemName); RFX_Byte(pFX, _T("[Category Number]"), m_CategoryNumber); RFX_Double(pFX, _T("[Unit Cost]"), m_UnitCost); } The CfieldExchange::outputColumn tells ODBC to store the table values of the current record found into these member fields. The CfieldExchange::param tells ODBC that on input to the query, load the current contents of the m_FindItemParam as part of the WHERE clause. We will examine how this works later on in the SalesView.cpp file. Listing for File: SalesDoc.h — Pgm08a The document class holds very little in this application, mostly the one CDatabase instance. #pragma once class SalesDoc : public CDocument { protected: // create from serialization only SalesDoc(); Windows MFC Programming II 429 DECLARE_DYNCREATE(SalesDoc) // Attributes public: CDatabase m_db; // the main database itself CDatabase* GetDb (); // returns ptr to the database for views // Overrides public: virtual BOOL OnNewDocument(); virtual void Serialize(CArchive& ar); // Implementation public: virtual ~SalesDoc(); #ifdef _DEBUG virtual void AssertValid() const; virtual void Dump(CDumpContext& dc) const; #endif protected: DECLARE_MESSAGE_MAP() }; Listing for File: SalesDoc.cpp — Pgm08a // SalesDoc.cpp : implementation of the SalesDoc class #include "stdafx.h" #include "Pgm08a.h" #include "SalesDoc.h" #ifdef _DEBUG #define new DEBUG_NEW #endif IMPLEMENT_DYNCREATE(SalesDoc, CDocument) BEGIN_MESSAGE_MAP(SalesDoc, CDocument) END_MESSAGE_MAP() SalesDoc::SalesDoc() { } SalesDoc::~SalesDoc() { } BOOL SalesDoc::OnNewDocument() { if (!CDocument::OnNewDocument()) return FALSE; try { m_db.OpenEx (_T("DBQ=Acme.mdb;DefaultDir=;Driver={Microsoft Access Driver (*.mdb)};DriverId=281;FIL=MS Access;FILEDSN=Acme.mdb.dsn;MaxBufferSize=2048;MaxScanRows=8;PageTimeout=5;Saf eTransactions=0;Threads=3;UID=admin;UserCommitSync=Yes;"), 0); } catch (CDBException *ptrex) { AfxMessageBox (ptrex->m_strError); ptrex->Delete (); } SetTitle ("Edit & Report Generator"); // install new caption Windows MFC Programming II 430 return TRUE; } CDatabase* SalesDoc::GetDb() { return &m_db; } void SalesDoc::Serialize(CArchive& ar) { if (ar.IsStoring()) { } else { } } In OnNewDocument, after calling the base class, it attempts to open the database. Each recordset will be passed the address of this opened instance of the database. Notice that all this was pretty much generated for us by the Class Wizard as we added the new classes. Yes, it is annoying to have to go through that very lengthy business to setup the next recordset, but ODBC is allowing you maximum flexibility to like several different databases together in your application, not just the same one. The only changes I added were the few for parameterization so that I can look up a specific item. I did take out the very lengthy full path specifications to the files and default paths. There is nothing new in the main frame class, just the default class that the wizard generated for the toolbar. The CWinApp Class — Forcing a Printer into Landscape Mode The basic Pgm08a class is just what the wizard generated with one exception. Since the app class owns the printer and since the width of this report demands landscape mode, I added a function to the app class to force the printer into landscape mode. How can an application enforce specific printer defaults? In this case, the report ought to be done in landscape mode. Therefore, whenever the user requests print or preview, begin by forcing the landscape mode. The user always has the option to select portrait orientation from the Print dialog. Recall that the application framework maintains the printer. Thus, the enforcement of printing options is best done from the CWinApp derived class using the PRINTDLG structure to gain access to those values to be enforced. In the Pgm08aApp I added the SetLandscape function which uses the GetPrinterDeviceDefaults to gain access to the current framework printer data. Simply lock the global handle and change away. Consult the on-line documentation for the precise layout of potential settings and their identifiers. For landscape, the hDevMode global contains dmOrientation which can be set to DMORIENT_LANDSCAPE or DM_ORIENTPORTRAIT. Windows MFC Programming II 431 void Pgm08aApp::SetLandscape () { // Get default printer settings. PRINTDLG pd; pd.lStructSize = (DWORD) sizeof (PRINTDLG); if (GetPrinterDeviceDefaults (&pd)) { DEVMODE FAR *pDevMode = (DEVMODE FAR*)::GlobalLock (m_hDevMode); if (pDevMode) { // Change printer settings in here. pDevMode->dmOrientation = DMORIENT_LANDSCAPE; ::GlobalUnlock (m_hDevMode); } } } The Printing Operations of Pgm08a At last the printing and preview process can be examined. Of course here’s the new printing scenario. Printing Situation #5: (Pgm08a) a. Reports are generated from a database accessed using the ODBC classes. b. Printing is done from within the document-view architecture. c. Both the App and Class Wizards are used. d. Printing and preview both use the MM_ANISOTROPIC mode. e. Fully operational Print, Print Setup, and Preview. f. By default, the printer is forced into Landscape mode to illustrate how to enforce particular printer settings, application wide. g. The report is a combination of columnar data and a bar chart; a combination of text and drawn graphics. h. The actual rendering of the page is done in RenderPage, not in OnDraw. i. Specific fonts are used that are not user chosen. j. No user margins are used; the report dictates the dimensions completely. k. The report has headers and page numbers. l. No provision is made for page overflow if there are too many items in a specific category. m. Precise page count given in Print Dialog, even the user changes the printer on a File|Print. n. Prints the precise user selectable page range. Windows MFC Programming II 432 The SalesView Class The SalesView class is derived from CRecordView, which comes from CFormView, requiring a dialog template as its form. In this case, the class handles three distinct actions. First, it handles the Sales table, allowing the user to scroll records, add, update, and delete records. Note that deleted records will be placed onto the clipboard so that a simple Undo can be done. This is roughly one third of the coding. Second, it handles print and print preview. Third, it must perform the extensive calculations for the report, displaying them onscreen, as in the Quick view option, or on the printer or preview window. Listing for File: SalesView.h — Pgm08a #pragma once class ODBCSales; // forward references class SalesDoc; /***************************************************************************/ /* */ /* Class SalesView: display the sales record set with printing */ /* */ /***************************************************************************/ class SalesView : public CRecordView { DECLARE_DYNCREATE(SalesView) /**************************************************************************/ /* */ /* Data Members */ /* */ /**************************************************************************/ protected: ODBCSales* m_pSet; int m_itemNumber; int m_quantity; // the main sales table record set // the item number purchased // the quantity of this item purchased bool bool int int bool // // // // // addInProgress; updateInProgress; m_recordCount; m_totalRecords; print_done; // sales data calcs bool calcs_setup; // int tot_cats; // int *tot_items_per_cat; // int tot_items; // int *itemnum_to_itemidx;// double *unitcost; // BYTE *cats; // CString *categories; // int max_item_idx; // short **itemidx_to_itemnum;// true while in the process of adding a record true while in the process of updating a record current record shown total number of records true when last page has been printed when TRUE, these are allocated and ready total number of categories total items per category the total number of items array of itemnums to their 2D table indices the unit costs of each item the category number of eac item array of the category names max number of items in all categories conversion to real item numbers Windows MFC Programming II double **sales; int int 433 // 2D table of sales avg_char_width; avg_char_height; /**************************************************************************/ /* */ /* Functions: */ /* */ /**************************************************************************/ protected: SalesView(); virtual ~SalesView(); // protected constructor used by dynamic creation public: enum { IDD = IDD_SALESVIEW }; // normal operational functions SalesDoc* GetDocument(); // gets a ptr to the doc class virtual void OnInitialUpdate(); // called first time after construct // recordset operations virtual void DoDataExchange(CDataExchange* pDX); // DDX/DDV support virtual CRecordset* OnGetRecordset(); virtual BOOL OnMove(UINT nIDMoveCommand); // move to desired record afx_msg void OnAddRecord(); // add a new record afx_msg void OnUpdateRecord(); // update this record afx_msg void OnDeleteRecord(); // delete this record afx_msg void OnBnClickedDoit(); // complete add or update afx_msg void OnBnClickedCancel(); // cancel add or update afx_msg void OnPasteRecord(); // undo a delete afx_msg void OnUpdatePaste(CCmdUI* pCmdUI); afx_msg void OnUpdateAddRecord(CCmdUI* pCmdUI); afx_msg void OnUpdateDeleteRecord(CCmdUI* pCmdUI); // the calculation functions afx_msg void OnQuickView(); // quick on-screen report view afx_msg void OnClearQuickView(); // clear report from screen BOOL DoCalculations (); // alloc and calc sales void FreeCalculations (); // frees the calc arrays void DisplayHeading (CDC*, CRect&, CString&, int); void DisplayText (CDC*, CRect&, BYTE); void DisplayGraph (CDC*, CRect&, BYTE); // printing functions virtual void OnPrepareDC(CDC* pDC, CPrintInfo* pInfo = NULL); // scale DC virtual void OnPrint(CDC* pDC, CPrintInfo* pInfo); // print a page void RenderPage (CDC*, int, CRect);// render one page of the report virtual BOOL OnPreparePrinting(CPrintInfo* pInfo); afx_msg void OnFilePrint(); // print the report afx_msg void OnFilePrintPreview(); // preview the report DECLARE_MESSAGE_MAP() ... Windows MFC Programming II 434 Handling the Recordset — SalesView.cpp Let’s examine the recordset handling for the Sales table first. Here I needed to make a number of changes from the default coding, because of the behavior of the ODBC classes. First, it is customary to present the client with a current record number out of the total number in the set. Unfortunately, ODBC has no easy way to handle this. Its own internal counts are often wrong, though it has a function to obtain this information, GetStatus, which returns a structure with the current record number and the total. Both of these numbers are often very wrong. We must maintain an accurate count. This means that when we first open the recordset, we need to move sequentially through each record, counting them as we go. Note that merely setting the ODBC recordset to the last record and then calling GetStatus does not give the total number of records in the set! User data of any type can be placed on the clipboard. In this case, I created a structure, OURCLIPDATA that stores the item number and quantity of the record being deleted. The ctor sets all members to an initial value, particularly the pointers. The arrays to hold the calculations will be dynamically allocated, so the dtor must free them, if they are still allocated when the app closes. DoDataExchange transfers the data from the recordset members into our view’s members first and then from the view’s members on into the form’s edit controls and vice versa. I highlighted this code in boldface. The Salesview.cpp file begins like this. Note that I am only showing it in shorter sections. To see the whole file, open the file in the samples folder. // SalesView.cpp : implementation file // #include #include #include #include #include #include #include #include #include #include "stdafx.h" "Pgm08a.h" "SalesView.h" "SalesDoc.h" "ODBCSales.h" "ODBCCountItemsPerCategory.h" "ODBCCategoryCount.h" "ODBCCategories.h" "ODBCGetItem.h" "ODBCItems.h" /***************************************************************************/ /* */ /* OURCLIPDATA: holds sales data to be transferred to/from the Clipboard */ /* */ /***************************************************************************/ struct OURCLIPDATA { short item; long qty; }; Windows MFC Programming II 435 IMPLEMENT_DYNCREATE(SalesView, CRecordView) /***************************************************************************/ /* */ /* SalesView Message Map */ /* */ /***************************************************************************/ BEGIN_MESSAGE_MAP(SalesView, CRecordView) ON_COMMAND(CM_ADDRECORD, ON_COMMAND(CM_UPDATE, ON_COMMAND(CM_DELETE, ON_COMMAND(CM_PASTENEW, ON_COMMAND(CM_QUICKVIEW, ON_COMMAND(CM_CLEARQUICKVIEW, ON_COMMAND(ID_FILE_PRINT, ON_COMMAND(ID_FILE_PRINT_DIRECT, ON_COMMAND(ID_FILE_PRINT_PREVIEW, ON_UPDATE_COMMAND_UI(CM_PASTENEW, ON_UPDATE_COMMAND_UI(CM_ADDRECORD, ON_UPDATE_COMMAND_UI(ID_EDIT_CUT, ON_BN_CLICKED(IDC_DOIT, ON_BN_CLICKED(IDC_CANCEL, END_MESSAGE_MAP() OnAddRecord) OnUpdateRecord) OnDeleteRecord) OnPasteRecord) OnQuickView) OnClearQuickView) OnFilePrint) OnFilePrint) OnFilePrintPreview) OnUpdatePaste) OnUpdateAddRecord) OnUpdateDeleteRecord) OnBnClickedDoit) OnBnClickedCancel) /***************************************************************************/ /* */ /* SalesView: constructor initializes, OnInitialUpdate opens the database */ /* */ /***************************************************************************/ SalesView::SalesView() : CRecordView(SalesView::IDD), m_itemNumber(0), m_quantity(0) , m_recordCount(0), m_totalRecords(0) { m_pSet = NULL; addInProgress = updateInProgress = FALSE; // not add or update print_done = FALSE; // when printing, tot pages = tot_cats // initialize to NULL the calc / printing members, counts, 1D, and 2D arrays calcs_setup = FALSE;// when TRUE, these are allocated tot_cats = 0; // total number of categories tot_items_per_cat = NULL; // total items per category tot_items = 0; // the total number of items itemnum_to_itemidx = NULL; // array of itemnums to their 2D table indices unitcost = NULL; // the unit costs of each item cats = NULL; // the category number of eac item categories = NULL; // the category string names max_item_idx = 0; // max number of items in all categories itemidx_to_itemnum = NULL; // conversion to real item numbers sales = NULL; // 2D table of sales } /***************************************************************************/ /* */ /* ~SalesData: destroy view by removing DB record set and any calc arrays */ /* */ /***************************************************************************/ Windows MFC Programming II 436 SalesView::~SalesView () { if (calcs_setup) FreeCalculations (); m_pSet->Close (); delete m_pSet; } /***************************************************************************/ /* */ /* DoDataExchange: xfer data to/from the record set and our controls */ /* */ /***************************************************************************/ void SalesView::DoDataExchange(CDataExchange* pDX) { CRecordView::DoDataExchange(pDX); DDX_FieldText(pDX, IDC_ITEMNUMBER, m_pSet->m_ItemNumber, m_pSet); DDX_FieldText(pDX, IDC_QUANTITY, m_pSet->m_QuantitySold, m_pSet); DDX_Text (pDX, IDC_RECORDCOUNT, m_recordCount); DDX_Text (pDX, IDC_TOTALRECORDS, m_totalRecords); } /***************************************************************************/ /* */ /* OnInitialUpdate: Set the dialog controls to read only in DB cannot updt */ /* */ /***************************************************************************/ void SalesView::OnInitialUpdate() { CDatabase* ptrdb = ((SalesDoc*) GetDocument())->GetDb(); m_pSet = new ODBCSales (); // allocate a new Sales Table record set m_pSet->Open (); // and open it; it then positions to first record // find the total number of records while (!m_pSet->IsEOF()) { m_totalRecords++; m_pSet->MoveNext (); } m_pSet->MoveFirst (); m_recordCount = 1; // set controls to read-only if the DB cannot handle updates if (!m_pSet->CanUpdate ()) { ((CEdit*) GetDlgItem (IDC_ITEMNUMBER))->SetReadOnly (TRUE); ((CEdit*) GetDlgItem (IDC_QUANTITY))->SetReadOnly (TRUE); } GetDlgItem(IDC_DOIT)->EnableWindow (FALSE); // enable the two action GetDlgItem(IDC_CANCEL)->EnableWindow (FALSE); // buttons on the view CRecordView::OnInitialUpdate(); } In OnInitialUpdate, obtain a pointer to the CDatabase class. Allocate a new ODBCSales recordset and then call its Open function. Now I must count all of the records in the set so that I can accurately display the total number of records in the set. Variable m_totalRecords will be maintained throughout the functions as well as the record number currently being displayed, m_recordCount. An alternative method would be to create another query, which would count them for us when run. Windows MFC Programming II 437 The function IsEOF() returns TRUE if the set contains no records or one has scrolled past the last record. The function MoveNext, moves the recordset down one row in the database set. MoveFirst moves to the very first record in the set. The function CanUpdate returns true if this recordset can support updating of records. If it cannot, then I grey out the two form view controls representing the item number and quantity, a sure signal to the user that he or she cannot alter the database. Finally, the two action buttons on the form view, DoIt and Cancel, which are used with adds and updates, are disabled. When adding or updating, these will be enabled. /***************************************************************************/ /* */ /* OnGetRecordset: obtain the CRecordSet pointer */ /* */ /***************************************************************************/ CRecordset* SalesView::OnGetRecordset() { return m_pSet; } /***************************************************************************/ /* */ /* GetDocument: returns pointer to SalesDoc */ /* */ /***************************************************************************/ SalesDoc* SalesView::GetDocument () { // non-debug version is inline ASSERT (m_pDocument->IsKindOf (RUNTIME_CLASS (SalesDoc))); return (SalesDoc*) m_pDocument; } Let’s examine next the sequences for making changes. These tie into how the user scrolls from record to record. The normal ODBC methods work this way. When one wishes to add a new record to a table, the function CanAppend returns TRUE if the recordset can support adding new records. If so, the AddNew function is called, which moves the recordset off the end, past the last record, inserting a record of all 0’s. When the data has been entered into the recordset members, Update must be called to physically store the data into the table. If any scrolling, that is moving to the next or previous record is done before the Update is called, the add is cancelled and the data discarded! This can be terribly confusing to the user of your applications! Hence, I modify the procedure by forcing the user to press the DoIt button when he or she is finished entering the new data. If they wish to cancel the operation, press the Cancel button. Any attempt to move to another record without pressing either Cancel or DoIt is prohibited. Likewise, if an update is already in progress or another add is in progress, refuse to allow this add request until the previous one is finished. This is done by maintaining the members addInProgress and updateInProgress. In this case, addInProgress is set to true and the two form view buttons are enabled. Windows MFC Programming II 438 /***************************************************************************/ /* */ /* OnAddRecord: begin add new record process - OnMove finishes the add or */ /* OnRefreshRecord cancels the add process */ /* */ /***************************************************************************/ void SalesView::OnAddRecord () { if (!m_pSet->CanAppend ()) { // can DB handle an add record? no AfxMessageBox ("Database doesn's support adding new records", MB_OK); return; } else if (addInProgress) { AfxMessageBox ("Finish current add first - press Do It or Cancel", MB_OK); return; } else if (updateInProgress) { AfxMessageBox ("Finish current update first - press Do It or Cancel", MB_OK); return; } m_pSet->AddNew (); // insert new record at end of DB addInProgress = true; // indicate add in progress GetDlgItem(IDC_DOIT)->EnableWindow (TRUE); // enable the two action GetDlgItem(IDC_CANCEL)->EnableWindow (TRUE); // buttons on the view UpdateData (FALSE); // force dialog's controls to be cleared } When the user has entered the new data, he or she presses the DoIt button. If an add is in progress, I call the Update recordset function here, after transferring the data from the controls into our data members and the recordset’s members. I turn off the addInProgress bool, increment the total number of records. Requery forces the recordset to reload its data, which is needed if the table has a key. MoveLast positions the current record to the one just added and UpdateData places that information back into the display edit controls. Finally, the two form view buttons are again disabled. If an update was in progress, after transferring the data from our controls into the data members, I call Update to finish the recordset update. /***************************************************************************/ /* */ /* OnBnClickedDoit: finish the add or update, if in progress */ /* */ /***************************************************************************/ void SalesView::OnBnClickedDoit() { if (addInProgress) { UpdateData (TRUE); // get the new data into our xfer bufs m_pSet->Update (); // pass them into the record set addInProgress = false; m_totalRecords++; m_recordCount = m_totalRecords; m_pSet->Requery (); // requery m_pSet->MoveLast (); // position last to this one UpdateData (FALSE); // set out display fields GetDlgItem(IDC_DOIT)->EnableWindow (FALSE); // disable the two action Windows MFC Programming II 439 GetDlgItem(IDC_CANCEL)->EnableWindow (FALSE); // buttons on the view } else if (updateInProgress) { UpdateData (TRUE); m_pSet->Update (); GetDlgItem(IDC_DOIT)->EnableWindow (FALSE); // disable the two action GetDlgItem(IDC_CANCEL)->EnableWindow (FALSE); // buttons on the view updateInProgress = false; } } Should the user change their mind and press the Cancel button, then I arbitrarily call MoveFirst, which ODBC uses to cancel the add automatically. Remember, AddNew puts one at the very end of the recordset. An update, on the other hand, leaves the recordset positioned on the current record which is to be updated. In both cases, the form view’s two buttons are then disabled. /***************************************************************************/ /* */ /* OnBnClickedCancel: cancel the add or update, if in progress */ /* */ /***************************************************************************/ void SalesView::OnBnClickedCancel() { if (addInProgress) { addInProgress = false; m_pSet->MoveFirst (); m_recordCount = 1; UpdateData (FALSE); GetDlgItem(IDC_DOIT)->EnableWindow (FALSE); GetDlgItem(IDC_CANCEL)->EnableWindow (FALSE); } else if (updateInProgress) { updateInProgress = false; m_pSet->MoveNext (); UpdateData (FALSE); GetDlgItem(IDC_DOIT)->EnableWindow (FALSE); GetDlgItem(IDC_CANCEL)->EnableWindow (FALSE); } } // disable the two action // buttons on the view // disable the two action // buttons on the view /***************************************************************************/ /* */ /* OnUpdateRecord: start the updating of the current record */ /* */ /***************************************************************************/ void SalesView::OnUpdateRecord() { if (!m_pSet->CanUpdate ()) { // can DB handle an add record? no AfxMessageBox ("Database doesn's support updating records", MB_OK); return; } else if (addInProgress) { AfxMessageBox ("Finish current add first - press Do It or Cancel", MB_OK); return; } else if (updateInProgress) { Windows MFC Programming II 440 AfxMessageBox ("Finish current update first - press Do It or Cancel", MB_OK); return; } m_pSet->Edit (); // insert new record at end of DB updateInProgress = true; // indicate add in progress GetDlgItem(IDC_DOIT)->EnableWindow (TRUE); // enable the two action GetDlgItem(IDC_CANCEL)->EnableWindow (TRUE); // buttons on the view UpdateData (FALSE); // force dialog's controls to be cleared } When the user wants to update a record, after CanUpdate returns TRUE indicating the recordset can support editing the current record, the Edit function is called. Once again, the edit is ended by a call to Update. Here, that will be enforced by pressing the DoIt button, not by scrolling to another record, which cancels the update. Deleting records demands that we also provide an Undo function, in case of accidental deletion. CanUpdate also returns TRUE if the recordset supports deletions. Of course, I must guard against a deletion while an add or update is in progress. If a delete is appropriate, I display a message box asking the user to confirm the deletion of the current record, showing them the data to be deleted. If they press the Yes button, then I make a copy of the data to put onto the clipboard. Allocate an instance of the OURCLIPDATA on Windows’ global heap and lock it down. Then, copy the two data items about to be deleted into this new instance. /***************************************************************************/ /* */ /* OnDeleteRecord: remove current record from the DB */ /* */ /***************************************************************************/ void SalesView::OnDeleteRecord () { if (!m_pSet->CanUpdate ()) { // can DB handle an add record? no AfxMessageBox ("Database doesn's support deleting records", MB_OK); return; } else if (addInProgress) { AfxMessageBox ("Finish current add first - press Do It or Cancel", MB_OK); return; } else if (updateInProgress) { AfxMessageBox ("Finish current update first - press Do It or Cancel", MB_OK); return; } if (AfxMessageBox ("Confirm deletion of this record", MB_YESNO) == IDYES) { // obtain global memory for our data to give to Clipboard HANDLE hbuffer = GlobalAlloc (GMEM_MOVEABLE, sizeof (OURCLIPDATA)); if (hbuffer==NULL) return; // copy current record to the global copy OURCLIPDATA *ptrbuf = (OURCLIPDATA*) GlobalLock (hbuffer); ptrbuf->item = m_pSet->m_ItemNumber; Windows MFC Programming II 441 ptrbuf->qty = m_pSet->m_QuantitySold; Then, the clipboard is opened, emptied (unless you wish to maintain more than one deleted record on the clipboard), the new data is installed. Notice that the identifier is called CF_PRIVATEFIRST. There is also another identifier, CF_PRIVATELAST. If you have more than one unique types of data, add one to CF_PRIVATEFIRST for each subsequent one. Calling Delete physically deletes the current record from the table or query. Next, I decrement our record count and try to find a new position within the record set to display. UpdateData ensures that whichever record now becomes the current one is displayed in the form view’s controls. // transfer global copy to the Clipboard VERIFY(OpenClipboard ()); VERIFY(::EmptyClipboard ()); VERIFY(::SetClipboardData (CF_PRIVATEFIRST, hbuffer)); VERIFY(::CloseClipboard ()); m_pSet->Delete (); // yes, delete this record } else // User changed mind, so leave return; m_totalRecords--; m_pSet->MoveNext (); // attempt to move to next if (m_pSet->IsEOF ()) { m_pSet->MoveLast (); // failed, so move to last m_recordCount = m_totalRecords; } if (m_pSet->IsBOF ()) { m_pSet->SetFieldNull (NULL);// failed, no records in DB m_recordCount = 1; } UpdateData (FALSE); // put cur rec into dialog } Now we can handle our override to the CRecordset’s OnMove function. The first action is to not do anything if there is either an add or update in progress. This forces the user to use the DoIt or Cancel buttons. If neither, then we can take normal actions. Once the move attempt has been attempted, the current record count must be adjusted, based upon the requested movement and what resulted. Only if something unusual occurred do I make use of the GetStatus function result. Normally, it is inaccurate. /***************************************************************************/ /* */ /* OnMove: complete any previous add request - then handle move request */ /* */ /***************************************************************************/ BOOL SalesView::OnMove (UINT nIDMoveCommand) { if (addInProgress || updateInProgress) { AfxMessageBox ("Finish current add/update first - press Do It or Cancel", MB_OK); return TRUE; } bool failed = false; Windows MFC Programming II 442 switch (nIDMoveCommand) { case ID_RECORD_PREV: m_pSet->MovePrev (); if (!m_pSet->IsBOF()) break; case ID_RECORD_FIRST: m_pSet->MoveFirst (); break; case ID_RECORD_NEXT: m_pSet->MoveNext (); if (!m_pSet->IsEOF()) break; break; case ID_RECORD_LAST: m_pSet->MoveLast (); break; default: failed = true; AfxMessageBox ("Unexpected move in record set. Try again", MB_OK); } CRecordsetStatus s; m_pSet->GetStatus (s); if (nIDMoveCommand == ID_RECORD_FIRST) m_recordCount = 1; else if (nIDMoveCommand == ID_RECORD_LAST) m_recordCount = m_totalRecords; else if (failed) m_recordCount = s.m_lCurrentRecord; else if (nIDMoveCommand == ID_RECORD_PREV) m_recordCount--; else if (nIDMoveCommand == ID_RECORD_NEXT) m_recordCount++; UpdateData (FALSE); return TRUE; } If the user wishes to Undo a deletion, I must paste that record back into the database. First, avoid doing anything if add or update is in progress. Next, check what clipboard format is present, if any. In this case, it must be our data, CF_PRIVATEFIRST. If so, open the clipboard and get the data. It will be on the Windows global heap. After closing the clipboard, lock that memory into a pointer to OURCLIPDATA. /***************************************************************************/ /* */ /* OnPasteRecord: Paste Clipboard copy into a new record in the DB */ /* */ /***************************************************************************/ void SalesView::OnPasteRecord () { if (addInProgress || updateInProgress) { AfxMessageBox ("Complete the add or update first", MB_OK); return; } // verify DB can add and that there is data on the Clipboard to paste if (m_pSet->CanAppend () && IsClipboardFormatAvailable (CF_PRIVATEFIRST)) { // retrieve the data from the Clipboard HANDLE hbuffer; VERIFY(OpenClipboard ()); VERIFY(hbuffer = GetClipboardData (CF_PRIVATEFIRST)); VERIFY(::CloseClipboard ()); OURCLIPDATA *ptrbuf = (OURCLIPDATA*) GlobalLock (hbuffer); Windows MFC Programming II 443 Next, call AddNew, fill up the recordset members from the clipboard instance, and finalize the add by calling Update. Next, adjust the total record count and the current record. Since the add is now at the end, Requery and MoveLast to get to the added record. UpdateData then transfers the data from the recordset into our members and then onto the form view controls. m_pSet->AddNew (); // make a new record for our paste m_pSet->m_ItemNumber = ptrbuf->item; // update the record from Clipboard m_pSet->m_QuantitySold = ptrbuf->qty; m_pSet->Update (); // force record to be updated m_totalRecords++; m_recordCount = m_totalRecords; m_pSet->Requery (); // requery m_pSet->MoveLast (); // position last to this one UpdateData (FALSE); // set our display fields GlobalUnlock (hbuffer); // pitch the Clipboard data GlobalFree (hbuffer); } } /***************************************************************************/ /* */ /* Command Enablers for: OnAddRecord, OnDeleteRecord, two Paste functions */ /* */ /***************************************************************************/ void SalesView::OnUpdatePaste (CCmdUI *pCmdUI) { pCmdUI->Enable (m_pSet->CanUpdate () && IsClipboardFormatAvailable (CF_PRIVATEFIRST)); } void SalesView::OnUpdateAddRecord (CCmdUI *pCmdUI) { pCmdUI->Enable (m_pSet->CanUpdate () && m_pSet->CanAppend ()); } void SalesView::OnUpdateDeleteRecord (CCmdUI *pCmdUI) { pCmdUI->Enable (m_pSet->CanUpdate ()); } Windows MFC Programming II 444 Printing Operations of SalesView In OnFilePrint, get a pointer to the app class and call the SetLandscape function. If the calculation arrays are still allocated, free them. DoCalculations is called to reallocate the arrays and perform the necessary calculations. With all the data now at hand to print, the base class is called to handle the rest. When the printing is done, the calculation arrays are freed and the recordset is repositioned to some known spot, such as the first record. /***************************************************************************/ /* */ /* OnFilePrint: print the sales report */ /* */ /***************************************************************************/ void SalesView::OnFilePrint () { // force printer into landscape mode Pgm08aApp *ptrapp = (Pgm08aApp*) AfxGetApp (); if (ptrapp) ptrapp->SetLandscape (); // note, the user could still set it back in the Print Dlg if desired if (calcs_setup) FreeCalculations (); // remove existing calcs, if any if (!DoCalculations ()) { // attempt to calc the sales FreeCalculations (); // failed, remove any arrays left return; } CRecordView::OnFilePrint (); // go print the report if (calcs_setup) FreeCalculations (); // remove arrays m_pSet->MoveFirst (); // put recordset back to first m_recordCount = 1; // since its lost its position UpdateData (FALSE); } /***************************************************************************/ /* */ /* OnPreparePrinting: set total pages and preview pages */ /* */ /***************************************************************************/ BOOL SalesView::OnPreparePrinting (CPrintInfo *pInfo) { if (pInfo->m_bPreview) { // for print preview pInfo->m_nNumPreviewPages = tot_cats; // give guess for total pages pInfo->SetMaxPage (1); // set max for one landscape page } pInfo->SetMaxPage (tot_cats); // for print, set total pages return DoPreparePrinting(pInfo); } /***************************************************************************/ /* */ /* OnPrint: print the current page of the report */ /* */ /***************************************************************************/ void SalesView::OnPrint(CDC *pDC, CPrintInfo *pInfo) { Windows MFC Programming II 445 int pagenum = pInfo->m_nCurPage; // retrieve the page to be rendered if (pagenum -1 > tot_cats) { // check for end of report print_done = TRUE; return; } OnPrepareDC (pDC, pInfo); // fix up the DC for rendering // construct the display page rectangle and convert it into logical coords CRect r; r.left = r.top = 0; r.right = pDC->GetDeviceCaps (HORZRES); r.bottom = pDC->GetDeviceCaps (VERTRES); pDC->DPtoLP ((CPoint*) &r, 2); RenderPage (pDC, pagenum, r); // go print this page } /***************************************************************************/ /* */ /* OnFilePrintPreview: preview the printed report */ /* */ /***************************************************************************/ void SalesView::OnFilePrintPreview () { // force the printer into landscape mode Pgm08aApp *ptrapp = (Pgm08aApp*) AfxGetApp (); if (ptrapp) ptrapp->SetLandscape (); if (calcs_setup) FreeCalculations (); // remove previous calcs, if any if (!DoCalculations ()) { // attempt to calc the sales FreeCalculations (); // failed, remove any arrays left return; } m_pSet->MoveFirst (); m_recordCount = 1; UpdateData (FALSE); CRecordView::OnFilePrintPreview (); // note the preview thread returns here long before preview is really done // so the free array problem is handled by always using calcs_setup BOOL and // freeing the array it it already exists before each new action } /***************************************************************************/ /* */ /* OnPrepareDC: set the continue printing flag - scale print/preview DCs */ /* */ /***************************************************************************/ void SalesView::OnPrepareDC (CDC *pDC, CPrintInfo *pInfo) { CRecordView::OnPrepareDC(pDC, pInfo); // separate printing from display cases and preview from print if (pInfo != NULL) { // here we are printing // set the continue printing flag - if previewing, always true if (pInfo->m_bPreview) pInfo->m_bContinuePrinting = TRUE; // but if printing, controlled by pages actually printed Windows MFC Programming II 446 else if (!print_done) pInfo->m_bContinuePrinting = TRUE; else pInfo->m_bContinuePrinting = FALSE; } if (pDC->IsPrinting ()) { // set screen size to that of a full screen CRect clrect (0, 0, GetSystemMetrics (SM_CXSCREEN), GetSystemMetrics (SM_CYSCREEN)); CSize clsize = clrect.Size(); // set the size of the screen for window ext // set the printer page size for viewport extent CSize pagesize = CSize (pDC->GetDeviceCaps (HORZRES), pDC->GetDeviceCaps (VERTRES)); // install mode and scaling effects pDC->SetMapMode (MM_ANISOTROPIC); pDC->SetWindowExt (clsize); pDC->SetViewportExt (pagesize); } } If we are actually printing, the above function uses scaling on both axes, scaling the screen dimensions to that of the printed page. Thus, one function, RenderPage, can be written to display to all three devices, the screen, the preview window, and the printer. RenderPage uses the page number to show as an index into the categories, by subtracting one to obtain that index. By being passed the rfull rectangle, which is set for the screen and is being scaled for the printer and preview, it merely displays the various lines. First, the headings are shown, then the column headings and the left columnar report. Lastly, the bar graph right side is displayed. /***************************************************************************/ /* */ /* RenderPage: renders one report page on the passed DC */ /* */ /***************************************************************************/ void SalesView::RenderPage (CDC *ptrdc, int pagenum, CRect rfull) { // display the heading across the page DisplayHeading (ptrdc, rfull, categories[pagenum - 1], pagenum); // calc the CRect rtext rtext.right DisplayText display area for column view and show that portion = rfull; = rtext.left + rtext.Width () / 3; (ptrdc, rtext, pagenum - 1); // calc the display area for the graph and show graph CRect rdraw = rfull; rdraw.left = rtext.right; DisplayGraph (ptrdc, rdraw, pagenum - 1); } Windows MFC Programming II 447 Quick View and the Calculations — ODBC Look Up Tables Quick view is a fast way to see on top of the form view just what the report will look like. This is only a convenience tool, since you can get a better view of it in print preview. However, it also shows you how the common RenderPage function can be so multipurposed. Here, it calculates the dimensions of the screen and passes those to RenderPage along with the view’s DC. Highlighted in boldface is the adjustment to have the report appear below the form view’s static text messages and buttons. To accumulate the total sales by category and item within each category from the Sales Table, is non-trivial. Yes, I could simplify the whole process and assume there are only three categories each with a maximum of nine items in them, but that is inflexible. It is far better to dynamically determine these values and dynamically allocate the requisite arrays. The ODBCCategoryCount record set contains the Access Query results which contain only one record, the count of the number of categories. Similarly, the ODBCCountItemsPerCategory query results in a record for each category, the number of items in that category. In other words, if the database query is already setup, an application can run that query and access its results just as if it were a table. So given these values, the two-dimensional arrays can be allocated. But what arrays are needed? Here enters a design approach. There are many ways to perform the task. One of the very simplest ways would be to construct another query that links the Sales Table’s item number to the Items Table and then links its category number to the Categories Table. The query result would contain all of the fields; the original item number and quantity along with the item string name with the unit cost along with the category number and its string name. If then sorted into category and item number order, not only can the accumulated sales figures be calculated directly, but also all the necessary strings are available for printing or display, such as the item name and category. But consider this, suppose that this is a large database with numerous categories and items; suppose further that the Sales Table had several hundred thousand entries in it. Can you see potential efficiency and memory problems with such an approach? In this application, I shall attempt to minimize the database activities in an effort to gain speed of execution. Thus, whenever the item name or category name is required, the corresponding table is searched for the corresponding information. What arrays and data items are required? tot_cats contains the total number of categories found from the ODBCCategoryCount query while tot_items contains the total number of items. Figuratively, tot_items_per_cat [tot_cats] holds the total items in each of the categories. Likewise, sales [tot_cats][tot_items_per_cat] contains the desired accumulated sales. However, two conversions are required. The item numbers in the Items Table range consecutively from 0 upwards. To access the sales array, an item index is required. Thus, the array itemnum_to_itemidx [tot_items] for each item number gives the index value to access the sales array so given the item number from the Sales Table, we can get the second index for the sales array. To get the category and unit cost, the arrays cat[tot_items] and unitcost[tot_items] are Windows MFC Programming II 448 required. When using the sales array results to display the information, we can sweep through the array for each category and for each item index, but how do we convert from the item index back to the item number? The array itemidx_to_itemnum[tot_cats][tot_items_per_cat] contains the linear item number. Obviously I do not want the second subscript of the two two-dimensional arrays to be a varying amount with each category, the first subscript. Instead max_item_idx is used and it contains the maximum number of items within a category across all categories. Finally there is one other design detail that must be understood. These arrays are dynamically allocated in response to the Quick View, print and preview operations. Further, print can be invoked from within the preview window. Care must be taken to delete the memory allocated and also to not recursively allocate these arrays. And then there is the OnFilePrintPreview problem. Recall that preview is implemented as a separate thread. Control returns back to our launching member once the preview window is up and running. The arrays cannot just be freed at that point or preview subsequently crashes. The solution I have adopted uses the BOOL calcs_setup which when TRUE implies that the arrays are already allocated and the calculations complete. Now the class destructor can delete the arrays what might be left after a preview operation followed by application shutdown. Upon entry to either print or quick view functions, if calcs_setup is TRUE, the potentially out of date arrays can be freed before being reallocated and the new sales calculated. Reexamine the SalesView header to see how I have defined these arrays. Then look at how the arrays are dynamically allocated, filled, and freed in the second part ot the implementation file for SalesView. /***************************************************************************/ /* */ /* OnQuickView: Show the first page of the report for inspection */ /* */ /***************************************************************************/ void SalesView::OnQuickView () { if (calcs_setup) FreeCalculations (); if (!DoCalculations ()) { FreeCalculations (); return; } // // // // remove arrays if they still exist attempt to calc the sales failed, remove any arrays left and abort QuickView CClientDC dc (this); CRect r; GetClientRect (&r); r.top += 250; // make a DC to view upon // setup a display rectangle // that does not interfeer with // dialog controls BYTE which_cat = 0; RenderPage (&dc, which_cat + 1, r); // you can adjust which page is viewed // render the one page on-screen FreeCalculations (); m_pSet->MoveFirst (); // remove calc arrays // put record set back to start Windows MFC Programming II m_recordCount = 1; UpdateData (FALSE); 449 // since it's lost its current postion } /***************************************************************************/ /* */ /* DisplayHeading: display the main report heading across the page top */ /* */ /***************************************************************************/ void SalesView::DisplayHeading (CDC *ptrdc, CRect &r, CString &category, int page) { ptrdc->SetBkMode (TRANSPARENT); ptrdc->SetBkColor (RGB (192, 192, 192)); TEXTMETRIC tm; // make to rather large fonts for heading and page number and category CFont fontbig; fontbig.CreateFont (36, 0, 0, 0, FW_BOLD, 0, 0, 0, DEFAULT_CHARSET, OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS, PROOF_QUALITY, FF_ROMAN | TMPF_TRUETYPE, "Times New Roman"); CFont fontmed; fontmed.CreateFont (24, 0, 0, 0, FW_BOLD, 0, 0, 0, DEFAULT_CHARSET, OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS, PROOF_QUALITY, FF_ROMAN | TMPF_TRUETYPE, "Times New Roman"); // calculate big font's average character dimensions ptrdc->SelectObject (&fontbig); ptrdc->GetTextMetrics (&tm); avg_char_width = tm.tmAveCharWidth; avg_char_height = tm.tmHeight + tm.tmExternalLeading; // display main header CRect rtop = r; rtop.bottom = r.top + avg_char_height; CString head ("Acme Sales by Category"); ptrdc->DrawText (head, &rtop, DT_CENTER); r.top += avg_char_height * 2; // set up page number and its location rectangle-wait for med font to show it char m[30]; wsprintf (m,"Page: %d", page); CRect rpage = rtop; rpage.OffsetRect (0, avg_char_height); // select medium font and calculate its average character dimensions ptrdc->SelectObject (&fontmed); ptrdc->GetTextMetrics (&tm); avg_char_width = tm.tmAveCharWidth; avg_char_height = tm.tmHeight + tm.tmExternalLeading; // show page number ptrdc->DrawText (m, &rpage, DT_CENTER); // show category line with gray stripe rtop = r; rtop.bottom = r.top + avg_char_height; Windows MFC Programming II 450 CBrush br; br.CreateSolidBrush (RGB (200, 200, 200)); CBrush *ptroldbr = ptrdc->SelectObject (&br); CPen *ptroldpen = (CPen*) ptrdc->SelectStockObject (NULL_PEN); rtop.top -= 4; // since text is at very top, add a spacer for aesthetics // show stripe ptrdc->Rectangle (&rtop); ptrdc->SelectObject (ptroldbr); ptrdc->SelectObject (ptroldpen); rtop.top += 4; // show category line over the top of the stripe ptrdc->TextOut (rtop.left, rtop.top, category); r.top += avg_char_height * 2; } /***************************************************************************/ /* */ /* DisplayText: render the text columnar listing on the left 1/3 of a page */ /* */ /***************************************************************************/ void SalesView::DisplayText (CDC *ptrdc, CRect &r, BYTE cat) { TEXTMETRIC tm; // make a bold and a regular smaller font CFont fontbold, fontnorm; fontbold.CreateFont (20, 0, 0, 0, FW_BOLD, 0, 0, 0, DEFAULT_CHARSET, OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS, PROOF_QUALITY, FF_ROMAN | TMPF_TRUETYPE, "Times New Roman"); fontnorm.CreateFont (20, 0, 0, 0, FW_NORMAL, 0, 0, 0, DEFAULT_CHARSET, OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS, PROOF_QUALITY, FF_ROMAN | TMPF_TRUETYPE, "Times New Roman"); ptrdc->SelectObject (&fontbold); ptrdc->GetTextMetrics (&tm); avg_char_width = tm.tmAveCharWidth; avg_char_height = tm.tmHeight + tm.tmExternalLeading; // rect r holds whole left 1/3 area // make rprod and rsls rects for the two columns of data CRect rsls = r; CRect rprod = r; rprod.right = rprod.right - avg_char_width * 11; rsls.left = rsls.right - avg_char_width * 11; rprod.bottom = rprod.top + avg_char_height; rsls.bottom = rsls.top + avg_char_height; // display the two column headings CString prod ("Product:"); ptrdc->TextOut (rprod.left, rprod.top, prod); rprod.OffsetRect (0, avg_char_height); CString sls ("Sales:"); ptrdc->TextOut (rsls.left, rsls.top, sls); rsls.OffsetRect (0, avg_char_height); // now underline the whole column heading line Windows MFC Programming II 451 ptrdc->PatBlt (r.left, r.top + avg_char_height - 6, r.Width (), 2, BLACKNESS); // select the main listing font ptrdc->SelectObject (&fontnorm); Here I need to input all items in the current category from the database and print their names and the total sales. I am given an array of all the item numbers in this category. What I don’t have is there corresponding names. While this could be done as a join query, here I will illustrate how to perform a look up operation. That is, given a specific item number, find its matching record in the Items table. To do this, I need a parameterized recordset, ODBCGetItems class. The one parameter to look up and match will be the current item number. The ODBC equivalent of the SQL WHERE clause is a string which looks like this: "[Item Number] = ?" When the query runs, the ? will be replaced with the value(s) of the parameters. Three members must be set: m_strFilter, m_nParams, and our own added member that holds the current item number to find, m_FindItemParam. These lines are highlighted below. Note they must be setup before the recordset is opened. Snapshot is used here, since no changes to the recordset are allowed. Each time a new item number occurs, the member is set to its value and the recordset function Requery is called. This same process is repeated in the bar graph drawing function, also highlighted in bold. // input all items in this category from the DB and print their names & sales // begin by getting a pointer to the database itself and then an instance of // the Items Table itself in which to look up the actual item name CDatabase *ptrdb = GetDocument ()->GetDb (); if (!ptrdb->IsOpen ()) return; BOOL ok = TRUE; // setup and open the required tables the two queries and the Items Table ODBCGetItem dbitems (ptrdb); dbitems.m_strFilter = "[Item Number] = ?"; dbitems.m_FindItemParam = "0"; dbitems.m_nParams = 1; try { dbitems.Open (CRecordset::snapshot, NULL, CRecordset::readOnly); } catch (CDBException *ptrex) { AfxMessageBox (ptrex->m_strError, MB_OK); ptrex->Delete (); ok = FALSE; } if (!ok) return; int i; short item; // for all items in this category, look up its name and print name and sales for (i=0; i<tot_items_per_cat[cat]; i++) { // convert item index back to item number in db item = itemidx_to_itemnum[cat][i]; Windows MFC Programming II 452 // set table to that item number char istr[20]; sprintf_s (istr, sizeof(istr), "%d", item); dbitems.m_FindItemParam = istr; dbitems.Requery(); // display the item name ptrdc->TextOut (rprod.left, rprod.top, dbitems.m_ItemName); rprod.OffsetRect (0, avg_char_height); // format and display the sales, right justified char s[15]; sprintf_s (s, sizeof(s), "%10.2lf", sales[cat][i]); ptrdc->TextOut (rsls.left, rsls.top, "$"); ptrdc->SetTextAlign (TA_RIGHT); ptrdc->TextOut (rsls.right, rsls.top, s); ptrdc->SetTextAlign (TA_LEFT); rsls.OffsetRect (0, avg_char_height); } dbitems.Close (); } /***************************************************************************/ /* */ /* DisplayGraph: render the bar chart on the right 2/3 of the page */ /* */ /***************************************************************************/ void SalesView::DisplayGraph (CDC *ptrdc, CRect &r, BYTE cat) { TEXTMETRIC tm; // make very small font CFont font; font.CreateFont (14, 0, 0, 0, FW_NORMAL, 0, 0, 0, DEFAULT_CHARSET, OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS, PROOF_QUALITY, FF_ROMAN | TMPF_TRUETYPE, "Times New Roman"); ptrdc->SelectObject (&font); ptrdc->GetTextMetrics (&tm); avg_char_width = tm.tmAveCharWidth; avg_char_height = tm.tmHeight + tm.tmExternalLeading; const const const const const int int int int int bar_height = 20; max_sls = 2000; big_ticks = 5; sml_ticks = 3; sml_tick_height = 5; // // // // // uniform height of all bars the maximum sales graphed number of vertical main lines num tiny ticks between the main lines tiny tick height // setup the product id side to the left of the chart and the graph rects CRect rprod = r; CRect rgraf = r; rprod.right = rprod.left + avg_char_width * 25; rprod.top += bar_height / 2; rprod.bottom = rprod.top + avg_char_height; rgraf.left = rprod.right + avg_char_width; rgraf.right -= avg_char_width * 3; int num = tot_items_per_cat[cat]; rgraf.bottom = rgraf.top + num * bar_height * 2; int w = rgraf.Width () / big_ticks; int ws = w / (sml_ticks + 1); Windows MFC Programming II // now draw the graph grid of vertical lines // and the ticks across the bottom in between the main vertical lines // and display the horizontal scale values below each main vertical line int i, j; int x, y; for (i=0; i<big_ticks+1; i++) { x = rgraf.left + i * w; for (j=0; j<sml_ticks; j++) { ptrdc->MoveTo (x + (j+1) * ws, rgraf.bottom); ptrdc->LineTo (x + (j+1) * ws, rgraf.bottom + sml_tick_height); } ptrdc->MoveTo (rgraf.left + i * w, rgraf.top); ptrdc->LineTo (rgraf.left + i * w, rgraf.bottom); char m[10]; wsprintf (m, "%d", i * max_sls / big_ticks); int xt = i ? avg_char_width * 2 : 0; ptrdc->TextOut (rgraf.left + i * w - xt, rgraf.bottom + 3, m); } ptrdc->MoveTo (rgraf.left, rgraf.bottom); ptrdc->LineTo (rgraf.right, rgraf.bottom); // set the initial x,y position for the first bar to be graphed to be // located just below the top of the graph vertical lines y = rgraf.top + bar_height / 2; x = rgraf.left; // gain access to the database CDatabase *ptrdb = GetDocument ()->GetDb (); if (!ptrdb->IsOpen ()) return; BOOL ok = TRUE; // setup and open the Items Table to be able to retrieve the item names ODBCGetItem dbitems (ptrdb); dbitems.m_strFilter = "[Item Number] = ?"; dbitems.m_FindItemParam = "0"; try { dbitems.Open (CRecordset::snapshot, NULL, CRecordset::readOnly); } catch (CDBException *ptrex) { AfxMessageBox (ptrex->m_strError, MB_OK); ptrex->Delete (); ok = FALSE; } if (!ok) return; ptrdc->SelectStockObject (BLACK_BRUSH); short item; int sls_w; // for each product, retrieve its name and display name and sales bar for (i=0; i<tot_items_per_cat[cat]; i++) { // convert item index into the table item number item = itemidx_to_itemnum[cat][i]; // find the item in the table char istr[20]; sprintf_s (istr, sizeof(istr), "%d", item); dbitems.m_FindItemParam = istr; dbitems.Requery(); ptrdc->SetTextAlign (TA_RIGHT); 453 Windows MFC Programming II 454 ptrdc->TextOut (rprod.right, rprod.top, dbitems.m_ItemName); ptrdc->SetTextAlign (TA_LEFT); rprod.OffsetRect (0, bar_height * 2); // calculate the bar width based on its sales sls_w = (int) (sales[cat][i] / max_sls * rgraf.Width ()); // and draw the slaes bar ptrdc->Rectangle (x, y, x + sls_w, y + bar_height); y += bar_height * 2; } dbitems.Close (); } /***************************************************************************/ /* */ /* DoCalculations: alloc single and 2D arrays, fills them, calcs sales */ /* */ /***************************************************************************/ // assumes the Categories Table is sorted into category number order // assumes the Items Table is sorted by category and by Item Number // and that item and category numbers are sequential from 0 // assumes a minimum of database activity to accumulate the sales BOOL SalesView::DoCalculations () { if (calcs_setup) return TRUE; // avoid recursion m_pSet->Requery (); // get latest changes, if any int i, j; tot_cats = 0; tot_items = 0; max_item_idx = 0; // total number of categories // the total number of items // max number of items in all categories // set up the main database object CDatabase *ptrdb = GetDocument ()->GetDb (); if (!ptrdb->IsOpen ()) return FALSE; BOOL ok = TRUE; // setup and open the required tables the two queries and the Items Table ODBCCategoryCount dbnumcat (ptrdb); ODBCCategories dbcat (ptrdb); ODBCCountItemsPerCategory dbnumitem (ptrdb); ODBCItems dbitems (ptrdb); try { dbnumcat.Open (); dbcat.Open (); dbnumitem.Open (); dbitems.Open (); } catch (CDBException *ptrex) { AfxMessageBox (ptrex->m_strError, MB_OK); ptrex->Delete (); ok = FALSE; } if (!ok) return FALSE; // get the total number of categories tot_cats = dbnumcat.m_CountOfCategoryNumber; Windows MFC Programming II 455 // allocate the number items in each category table tot_items_per_cat = new int [tot_cats]; tot_items = 0; // the total number of items // go get the number of items per category for (i=0; i<tot_cats; i++) { j = dbnumitem.m_CategoryNumber; tot_items_per_cat[j] = dbnumitem.m_CountOfItemNumber; tot_items += tot_items_per_cat[j]; dbnumitem.MoveNext (); } dbnumitem.Close (); dbnumcat.Close (); // allocate and load the categories string array categories = new CString [tot_cats]; for (i=0; i<tot_cats; i++) { categories[i] = dbcat.m_Category; dbcat.MoveNext (); } dbcat.Close (); // allocate the conversion array of item numbers to their 2D table indexes // and the unit costs of each item and its category number itemnum_to_itemidx = new int [tot_items]; unitcost = new double [tot_items]; cats = new BYTE [tot_items]; // get the maximum number of items of all categories for 2D table bounds max_item_idx = 0; for (i=0; i<tot_cats; i++) if (max_item_idx < tot_items_per_cat[i]) max_item_idx = tot_items_per_cat[i]; // allocate the 2D tables for // sales[max cats][max items] the total item sales // itemidx_to_itemnum [max cats][max items] conversion to real item number sales = new double* [tot_cats]; itemidx_to_itemnum = new short* [tot_cats]; for (i=0; i<tot_cats; i++) { sales[i] = new double [max_item_idx]; itemidx_to_itemnum[i] = new short [max_item_idx]; } int idx = 0; BYTE oldcat = dbitems.m_CategoryNumber; BYTE cat; // setup the item number to 2D table index conversions and the reverse // along with the unitcost of each item and its category number while (!dbitems.IsEOF ()) { cat = dbitems.m_CategoryNumber; if (oldcat != cat) { oldcat = cat; idx = 0; } itemidx_to_itemnum[cat][idx] = dbitems.m_ItemNumber; itemnum_to_itemidx[dbitems.m_ItemNumber] = idx; Windows MFC Programming II 456 unitcost[dbitems.m_ItemNumber] = dbitems.m_UnitCost; cats[dbitems.m_ItemNumber] = cat; idx++; dbitems.MoveNext (); } dbitems.Close (); // clear sales array for (i=0; i<tot_cats; i++) for (j=0; j<max_item_idx; j++) sales[i][j] = 0.; // accumulate the total sales by category and item number m_pSet->MoveFirst (); while (!m_pSet->IsEOF ()) { short item = m_pSet->m_ItemNumber; cat = cats[item]; idx = itemnum_to_itemidx[item]; sales [cat][idx] += unitcost[item] * m_pSet->m_QuantitySold; m_pSet->MoveNext (); } calcs_setup = TRUE; return TRUE; } The dynamically allocated array coding is straightforward as is the sequential movement through the different recordsets, storing the results in the temporary calculation arrays. Again, this would be easier done with SQL joins. /***************************************************************************/ /* */ /* FreeCalculations: remove all of the calculation arrays */ /* */ /***************************************************************************/ void SalesView::FreeCalculations () { if (!calcs_setup) return; // nothing to do int i; // free the two 2D arrays if (sales) { for (i=0; i<tot_cats; i++) delete [] sales[i]; delete [] sales; sales = NULL; } if (itemidx_to_itemnum) { for (i=0; i<tot_cats; i++) delete [] itemidx_to_itemnum[i]; delete [] itemidx_to_itemnum; itemidx_to_itemnum = NULL; } // free all 1D arrays if (tot_items_per_cat) { delete [] tot_items_per_cat; tot_items_per_cat = NULL; } Windows MFC Programming II 457 if (itemnum_to_itemidx) { delete [] itemnum_to_itemidx; itemnum_to_itemidx = NULL; } if (unitcost) { delete [] unitcost; unitcost = NULL; } if (cats) { delete [] cats; cats = NULL; } if (categories) { delete [] categories; categories = NULL; } calcs_setup = FALSE; } /***************************************************************************/ /* */ /* OnClearQuickView: removes the QuickView from the screen */ /* */ /***************************************************************************/ void SalesView::OnClearQuickView () { Invalidate (); } Perhaps the biggest drawback of the ODBC system is the awkward methods needed to look up a specific value in a table or query. Windows MFC Programming II 458 The Data Access Objects System (DAO) The older DAO system solves this with direct loop up functions and is easier to code. However, they are only for Access mdb databases and are “obsoleted” as far as Microsoft is concerned. Still, they offer the fastest access of mdb files. However, they are limited to earlier versions of Access, up to Office2000, and will not be available in 64-bit Vista. I am including this older version of our fancy database report generator and recordset editor for those who have older mdb’s and want the fastest db access. If you have no need of this technology, please skip this entire section. I will present the coding, but most of the commentary of Pgm08a applies totally to Pgm08b exactly. One replaces the ODBC classes with the totally parallel DAO classes. The DOA Classes Used in Pgm08b The MFC has a number of DOA classes whose prefixes all begin with CDao; three key classes are utilized here. The CDaoDatabase class encapsulates the entire database, here Acme.mdb. It represents the connection of an application to the database through which information can be interchanged. Generally, an instance of the CDaoDatabase class is constructed and used; we do not usually need to derive a database class from it. Think of the CDaoDatabase class as being the glue that ties all of the tables, queries, and so forth together. Some key member functions include IsOpen, Open, Close and CanUpdate. Other functions, such as Create, builds a new database, run action queries, delete tables and queries, and return key information about the database, including the number of tables, queries, and relations between tables. For our purposes in this sample, the database already exists; we just want to access its tables and queries. CDaoRecordSet encapsulates access to individual tables and queries. This is the workhorse class; for each table or query result that the application wants to use, derive a specific CDaoRecordSet class to handle the specifics of that table or query result table. This class maps the table’s fields into class member fields and handled the data transfer to and from the database and the class instance members that represent them. In this sample, there are five tables and query result tables; so therefore there are five classes derived from CDaoRecordSet. The record set, as its name implies, operates on a record basis, providing access to the record’s member fields, updating that data, adding new records, deleting records, searching for matching records, and normal scrolling operations through the records themselves. In addition to the obvious Open and Close functions, CanAppend and CanUpdate return TRUE if the record set and handle additions and updates (including deletes); IsEOF and IsBOF return TRUE when the end of file or the beginning of the file is reached. MoveNext, MovePrev, MoveFirst, MoveLast and Move alter the current position in the table and cause the Windows MFC Programming II 459 transfer of that data to the class member variables; note Move adjusts the position by a specific number of records from the current location. DoFieldExchange actually handles the transfer of the table’s current record’s members to the class instance’s members. Seek is used to find a specific record in the table by using the table’s index. AddNew, Delete, and Edit provide the data editing capabilities while Update completes the addition or update by saving the changes to the table. Finally, CDaoRecordView adds support for the doc-view architecture by providing a view in which to display the CDaoRecordSet data members. Specifically, it is a form view displaying one record at a time in a form view that is built from a dialog template whose controls are to contain the record set’s data. It uses normal dialog data exchange (DDX) to transfer the data to and from the dialog controls and the fields of the record set. Thus, the class provides the merged functionality of a view and its associated record set. The OnMove function is used to funnel all form view record movement request to the record set via specific command IDs: ID_RECORD_FIRST, ID_RECORD_LAST, ID_RECORD_NEXT, and ID_RECORD_PREV. Since the wizards no longer support these classes, you will need to do the coding by hand, emulating my classes. Please note that there are three record set types available: Snapshot, Dynaset, and Table. The default is Dynaset which can handle both tables and query result tables; it is updatable. Table type can only handle tables and is updatable; more importantly, the Seek function can be used to find matching records using the table’s indices. The Table type is available only with Access and other ISAM databases that provide index support. The Snapshot type is not updatable and maintains a static snapshot of the data. Usually Dynaset is the option desired; however, in this application, the Categories and Items tables use the Table option so that the Seek for matching records can be used. However, at this point in the application construction, the Sales Table is used in the view class; Dynaset is chosen. Next, build the form’s dialog to hold the item number and its corresponding quantity sold. The dialog controls must be bound with the view class’ associated record set member variables, just as in ODBC. The dialog resource points to IDD_PGM08B_FORM; the base class is CDaoRecordView; the foreign class representing the DAO record set class is DAOSales. The current instance of our DAOSales object is pointed to by m_pSet. The binding that must be done is to tie each of the dialog controls to its counterpart in the DAOSales foreign database record set class member variables. Thus whenever the current record in the database changes, automatic data exchange places those data into the dialog controls and vice versa. When the user makes changes in the dialog controls, the data transfer mechanism automatically places the new data into the DAOSales data members and then into the database table itself. Windows MFC Programming II 460 The Implementation of the Five CDaoRecordSet Classes in Pgm08b The actual implementation of all five CDaoRecordSet classes is nearly identical save for the actual data members and corresponding DDX macros. They parallel exactly their ODBC versions. Listing for File: DAOSales.h — Pgm08b class DAOSales : public CDaoRecordset { /**************************************************************************/ /* */ /* Data Members */ /* */ /**************************************************************************/ public: // Field/Param Data short m_Item_Number; long m_Quantity_Sold; // item number // quantity sold /**************************************************************************/ /* */ /* Functions */ /* */ /**************************************************************************/ public: DAOSales(CDaoDatabase* pDatabase = NULL); DECLARE_DYNAMIC(DAOSales) public: virtual CString GetDefaultDBName(); // return db name virtual CString GetDefaultSQL(); // return table name virtual void DoFieldExchange(CDaoFieldExchange* pFX); // RFX support #ifdef _DEBUG virtual void AssertValid() const; virtual void Dump(CDumpContext& dc) const; #endif }; Notice that the sales data members are called m_Item_Number and m_Quantity_Sold. Further, notice the data types: short and long. The data transfer mechanism handles the conversion of the short and long into strings for the Edit control automatically. This is true for all of the other data types found in the database. Windows MFC Programming II 461 Listing for File: DAOSales.cpp — Pgm08b ... IMPLEMENT_DYNAMIC(DAOSales, CDaoRecordset) /***************************************************************************/ /* */ /* DAOSales: access the Sales Table */ /* */ /***************************************************************************/ DAOSales::DAOSales (CDaoDatabase* pdb) : CDaoRecordset(pdb) { m_Item_Number = 0; m_Quantity_Sold = 0; m_nFields = 2; m_nDefaultType = dbOpenDynaset; } /***************************************************************************/ /* */ /* GetDefaultDBName: returns the database name */ /* */ /***************************************************************************/ CString DAOSales::GetDefaultDBName () { return _T("Acme.mdb"); } /***************************************************************************/ /* */ /* GetDefaultSQL: returns the Sales Table name */ /* */ /***************************************************************************/ CString DAOSales::GetDefaultSQL () { return _T("[Sales]"); } /***************************************************************************/ /* */ /* DoFieldExchange: xfer data to/from the database and the record set */ /* */ /***************************************************************************/ void DAOSales::DoFieldExchange (CDaoFieldExchange* pFX) { pFX->SetFieldType(CDaoFieldExchange::outputColumn); DFX_Short(pFX, _T("[Item Number]"), m_Item_Number); DFX_Long(pFX, _T("[Quantity Sold]"), m_Quantity_Sold); } ... The constructor initializes the data members and most importantly sets the open style to dbOpenDynaset. Be sure to change this to dbOpenTable in the DAOGetCat and DAOGetItem classes so that their indices can be searched. GetDafaultDBName returns the database name while GetDefaultSQL returns the table or query name. Finally, the DoFieldExchange handles the transfer of data from the database table to the member variables. Notice how the macros tie Windows MFC Programming II 462 the Access Sales table member names to the class member variables. Note the _T is the generic text mapping macro converting the string into a generic text string. For more information, see the on-line help topic Data Type Mappings. Here are the other CDaoRecordset classes, wrapping the other tables and queries. Listing for DAOGetCat .h and .cpp — Pgm08b class DAOGetCat : public CDaoRecordset { public: // Field/Param Data BYTE m_Category_Number; // category number CString m_Category; // category name public: DAOGetCat(CDaoDatabase* pDatabase = NULL); DECLARE_DYNAMIC(DAOGetCat) public: virtual CString GetDefaultDBName(); // returns database name virtual CString GetDefaultSQL(); // returns table name virtual void DoFieldExchange(CDaoFieldExchange* pFX); // RFX support ... #include "stdafx.h" #include "Pgm08b.h" #include "DAOGetCat.h" ... IMPLEMENT_DYNAMIC(DAOGetCat, CDaoRecordset) /***************************************************************************/ /* */ /* DAOGetCat: access the Category Table */ /* */ /***************************************************************************/ DAOGetCat::DAOGetCat (CDaoDatabase* pdb) : CDaoRecordset(pdb) { m_Category_Number = 0; m_Category = _T(""); m_nFields = 2; m_nDefaultType = dbOpenTable; // open for seeks and queries } /***************************************************************************/ /* */ /* GetDefaultDBName: returns the name of the database */ /* */ /***************************************************************************/ CString DAOGetCat::GetDefaultDBName () { return _T("Acme.mdb"); } Windows MFC Programming II 463 /***************************************************************************/ /* */ /* GetDefaultSQL: returns the name of the Table in the DB */ /* */ /***************************************************************************/ CString DAOGetCat::GetDefaultSQL () { return _T("[Categories]"); } /***************************************************************************/ /* */ /* DoFieldExchange: xfer data to/from the database and the recordset */ /* */ /***************************************************************************/ void DAOGetCat::DoFieldExchange (CDaoFieldExchange* pFX) { pFX->SetFieldType(CDaoFieldExchange::outputColumn); DFX_Byte(pFX, _T("[Category Number]"), m_Category_Number); DFX_Text(pFX, _T("[Category]"), m_Category); } Listing for DAOGetItem .h and .cpp — Pgm08b class DAOGetItem : public CDaoRecordset { public: // Field/Param Data short m_Item_Number; // item number CString m_Item_Name; // item name BYTE m_Category_Number; // corresponding category number double m_Unit_Cost; // unit cost of this item public: DAOGetItem(CDaoDatabase* pDatabase = NULL); DECLARE_DYNAMIC(DAOGetItem) public: virtual CString GetDefaultDBName(); // return database name virtual CString GetDefaultSQL(); // return table name virtual void DoFieldExchange(CDaoFieldExchange* pFX); // RFX support ... #include "stdafx.h" #include "Pgm08b.h" #include "DAOGetItem.h" ... IMPLEMENT_DYNAMIC(DAOGetItem, CDaoRecordset) /***************************************************************************/ /* */ /* DAOGetItem: Access the Items Table */ /* */ /***************************************************************************/ DAOGetItem::DAOGetItem (CDaoDatabase* pdb) : CDaoRecordset(pdb) { m_Item_Number = 0; m_Item_Name = _T(""); Windows MFC Programming II 464 m_Category_Number = 0; m_Unit_Cost = 0.0; m_nFields = 4; m_nDefaultType = dbOpenTable; // open for specific seeks or queries } /***************************************************************************/ /* */ /* GetDafaultDBName: returns the DB name in use */ /* */ /***************************************************************************/ CString DAOGetItem::GetDefaultDBName () { return _T("Acme.mdb"); } /***************************************************************************/ /* */ /* GetDefaultSQL: returns the table name */ /* */ /***************************************************************************/ CString DAOGetItem::GetDefaultSQL () { return _T("[Items]"); } /***************************************************************************/ /* */ /* DoFieldExchange: xfer to/from the DB Table and our record set */ /* */ /***************************************************************************/ void DAOGetItem::DoFieldExchange (CDaoFieldExchange* pFX) { pFX->SetFieldType(CDaoFieldExchange::outputColumn); DFX_Short(pFX, _T("[Item Number]"), m_Item_Number); DFX_Text(pFX, _T("[Item Name]"), m_Item_Name); DFX_Byte(pFX, _T("[Category Number]"), m_Category_Number); DFX_Double(pFX, _T("[Unit Cost]"), m_Unit_Cost); } Listing for DAONumCat .h and .cpp — Pgm08b class DAONumCat : public CDaoRecordset { public: // Field/Param Data long m_CountOfCategory_Number; // number of categories public: DAONumCat(CDaoDatabase* pDatabase = NULL); DECLARE_DYNAMIC(DAONumCat) public: virtual CString GetDefaultDBName(); // return database name virtual CString GetDefaultSQL(); // return query table name virtual void DoFieldExchange(CDaoFieldExchange* pFX); // RFX support Windows MFC Programming II 465 ... #include "stdafx.h" #include "Pgm08b.h" #include "DAONumCat.h" IMPLEMENT_DYNAMIC(DAONumCat, CDaoRecordset) /***************************************************************************/ /* */ /* DAONumCat: access the query results from Category Count Table */ /* */ /***************************************************************************/ DAONumCat::DAONumCat (CDaoDatabase* pdb) : CDaoRecordset(pdb) { m_CountOfCategory_Number = 0; m_nFields = 1; m_nDefaultType = dbOpenDynaset; } /***************************************************************************/ /* */ /* GetDefaultDBName: returns the name of the database */ /* */ /***************************************************************************/ CString DAONumCat::GetDefaultDBName () { return _T("Acme.mdb"); } /***************************************************************************/ /* */ /* GetDefaultSQL: returns the name of the query, Category Count */ /* */ /***************************************************************************/ CString DAONumCat::GetDefaultSQL() { return _T("[Category Count]"); } /***************************************************************************/ /* */ /* DoFieldExchange: xfer data to/from the database and the recordset */ /* */ /***************************************************************************/ void } DAONumCat::DoFieldExchange (CDaoFieldExchange* pFX) { pFX->SetFieldType(CDaoFieldExchange::outputColumn); DFX_Long(pFX, _T("[CountOfCategory Number]"), m_CountOfCategory_Number); Windows MFC Programming II 466 Listing for DAONumItem .h and .cpp — Pgm08b class DAONumItem : public CDaoRecordset { public: // Field/Param Data BYTE m_Category_Number; // category number long m_CountOfItem_Number; // number of items in this category public: DAONumItem(CDaoDatabase* pDatabase = NULL); DECLARE_DYNAMIC(DAONumItem) public: virtual CString GetDefaultDBName(); // return database name virtual CString GetDefaultSQL(); // return table name virtual void DoFieldExchange(CDaoFieldExchange* pFX); // RFX support ... #include "stdafx.h" #include "Pgm08b.h" #include "DAONumItem.h" ... IMPLEMENT_DYNAMIC(DAONumItem, CDaoRecordset) /***************************************************************************/ /* */ /* DAONumItem: access the query results from Count Items Per Category */ /* */ /***************************************************************************/ DAONumItem::DAONumItem (CDaoDatabase* pdb) : CDaoRecordset(pdb) { m_Category_Number = 0; m_CountOfItem_Number = 0; m_nFields = 2; m_nDefaultType = dbOpenDynaset; } /***************************************************************************/ /* */ /* GetDefaultDBName: returns the name of the database */ /* */ /***************************************************************************/ CString DAONumItem::GetDefaultDBName () { return _T("Acme.mdb"); } /***************************************************************************/ /* */ /* GetDefaultSQL: returns the query results table name */ /* */ /***************************************************************************/ CString DAONumItem::GetDefaultSQL () { return _T("[Count Items Per Category]"); } /***************************************************************************/ Windows MFC Programming II 467 /* */ /* DoFieldExchange: xfer data to/from the database and the recordset */ /* */ /***************************************************************************/ void DAONumItem::DoFieldExchange (CDaoFieldExchange* pFX) { pFX->SetFieldType(CDaoFieldExchange::outputColumn); DFX_Byte(pFX, _T("[Category Number]"), m_Category_Number); DFX_Long(pFX, _T("[CountOfItem Number]"), m_CountOfItem_Number); } Listing for File: stdafx.h — Pgm08a To get rid of the hundred plus function is deprecated warning messages, add the pragma in bold. #pragma once #define WINVER 0x0500 // Win2000 or better #define VC_EXTRALEAN headers // Exclude rarely-used stuff from Windows #include <afxwin.h> // MFC core and standard components #include <afxext.h> // MFC extensions #include <afxdao.h> // MFC DAO database classes #ifndef _AFX_NO_AFXCMN_SUPPORT #include <afxcmn.h> // MFC support for Windows 95 Common Controls #endif // _AFX_NO_AFXCMN_SUPPORT #pragma warning(disable:4995) Listing for SalesDoc .h and .cpp files — Pgm08a class SalesDoc : public CDocument { public: CDaoDatabase db; // the main database protected: SalesDoc(); // create from serialization only DECLARE_DYNCREATE(SalesDoc) public: CDaoDatabase* GetDB (); virtual BOOL OnNewDocument(); virtual void DeleteContents(); virtual ~SalesDoc(); ... // returns a ptr to the main database /***************************************************************************/ /* */ /* OnNewDocument: framework calls here - open our specific database */ /* */ /***************************************************************************/ BOOL SalesDoc::OnNewDocument () { Windows MFC Programming II 468 if (!CDocument::OnNewDocument()) return FALSE; // attempt to open our database CString dbname = "Acme.mdb"; try { db.Open (dbname); } catch (CDaoException *ptrex) { AfxMessageBox (ptrex->m_pErrorInfo->m_strDescription); ptrex->Delete (); } SetTitle ("Edit & Report Generator"); // install new caption return TRUE; } /***************************************************************************/ /* */ /* DeleteContents: close the database */ /* */ /***************************************************************************/ void SalesDoc::DeleteContents () { if (db.IsOpen ()) db.Close (); CDocument::DeleteContents(); } /***************************************************************************/ /* */ /* GetDB: returns pointer to the main database */ /* */ /***************************************************************************/ CDaoDatabase* return &db; } SalesDoc::GetDB () { Windows MFC Programming II 469 Adding, Updating and Deleting Records; User-defined Data on the Clipboard Next, the Sales update process must be installed so that the user can add, edit, and delete records. Specifically, the current record being displayed can be copied to the clipboard. Then that record could be pasted either over an existing record replacing its contents or it could be pasted as a new additional record. From the left are buttons to Add, Delete, Copy this record to the Clipboard, Paste Over this record, and Paste as a new record. The CDaoRecordSet has a rather large number of functions available. CanUpdate and CanAppend return TRUE if edit and delete or add options are supported. Of these, the edit or update record is the simplest with record addition the trickiest. The SalesView class is the lengthiest and it can easily be broken into three sections: code dealing with the update process, code dealing with the calculation of the accumulated sales, and code dealing with displaying or printing the sales report. I present the SalesView class header first so that the overview is present in one place. Then I shall examine each of the three sections in detail, presented over three separate listings. Listing for File: SalesView.h — Pgm08b ... class SalesView : public CDaoRecordView { protected: BOOL BOOL add_mode; print_done; // TRUE while in the process of adding records // TRUE when last page has been printed // sales data calcs BOOL calcs_setup; // int tot_cats; // int *tot_items_per_cat; // int tot_items; // int *itemnum_to_itemidx;// double *unitcost; // BYTE *cats; // CString *categories; // int max_item_idx; // short **itemidx_to_itemnum;// double **sales; // int int when TRUE, these are allocated and ready total number of categories total items per category the total number of items array of itemnums to their 2D table indices the unit costs of each item the category number of eac item array of the category names max number of items in all categories conversion to real item numbers 2D table of sales avg_char_width; avg_char_height; /**************************************************************************/ /* */ /* Functions: */ /* */ /**************************************************************************/ Windows MFC Programming II 470 protected: SalesView(); // create from serialization only DECLARE_DYNCREATE(SalesView) BOOL void void void void void DoCalculations (); // alloc and calc sales FreeCalculations (); // frees the calc arrays RenderPage (CDC*, int, CRect); // render one page of the report DisplayHeading (CDC*, CRect&, CString&, int); DisplayText (CDC*, CRect&, BYTE); DisplayGraph (CDC*, CRect&, BYTE); public: SalesDoc* GetDocument(); // gets a ptr to the doc class enum { IDD = IDD_PGM08A_FORM }; DAOSales* m_pSet; virtual CDaoRecordset* OnGetRecordset(); // get access to the record set virtual BOOL PreCreateWindow(CREATESTRUCT& cs); // set window style virtual BOOL OnMove(UINT nIDMoveCommand); // finish the add record process virtual void OnPrepareDC(CDC* pDC, CPrintInfo* pInfo = NULL); // scale DC protected: virtual void virtual void virtual BOOL virtual void DoDataExchange(CDataExchange* pDX); // DDX/DDV support OnInitialUpdate(); // set controls for read only OnPreparePrinting(CPrintInfo* pInfo); // set page numbers OnPrint(CDC* pDC, CPrintInfo* pInfo); // print a page public: virtual ~SalesView(); // remove calc arrays, and close db ... protected: afx_msg void OnAddRecord(); // add a new record afx_msg void OnPasteNewRecord(); // add via clipboard to a new record afx_msg void OnCopyRecord(); // copy this record to clipboard afx_msg void OnDeleteRecord(); // delete this record afx_msg void OnPasteOver(); // paste over this record afx_msg void OnRefreshRecord(); // abort change to this record afx_msg void OnQuickView(); // quick on-screen report view afx_msg void OnClearQuickView(); // clear report from screen afx_msg void OnFilePrint(); // print the report afx_msg void OnFilePrintPreview(); // preview the report afx_msg void OnUpdatePasteOver(CCmdUI* pCmdUI); afx_msg void OnUpdatePasteNew(CCmdUI* pCmdUI); afx_msg void OnUpdateAddRecord(CCmdUI* pCmdUI); afx_msg void OnUpdateDeleteRecord(CCmdUI* pCmdUI); DECLARE_MESSAGE_MAP() }; #ifndef _DEBUG // debug version in SalesView.cpp inline SalesDoc* SalesView::GetDocument () { return (SalesDoc*) m_pDocument; } #endif As a footnote, notice how in line functions are done. The GetDocument function is created in line in release versions but is not in debug versions so that you can trace through its code. Windows MFC Programming II 471 Since we wish only to examine the update process, only two data members are needed. m_pSet is a pointer to our DAOSales instance and add_mode is a BOOL which is TRUE during the add record process. Again, I kept the wizard generated names; you could substitute your own if desired. The constructor allocates a new instance of the DAOSales record set and opens it. add_mode is set to FALSE, indicating no add is in progress. In the destructor, the record set is closed and deleted. DoDataExchange transfers the data to/from the record set and our dialog controls. In OnInitialUpdate I added a nice touch. If the database is read-only, I force our dialog controls to also be read-only. CanUpdate returns TRUE if the database can handle edits and deletions. The implementation of edits is the simplest point to enter the coding sequences. Listing for File: SalesView.cpp — Part 1 — Pgm08b ... /***************************************************************************/ /* */ /* OURCLIPDATA: holds sales data to be transferred to/from the Clipboard */ /* */ /***************************************************************************/ struct OURCLIPDATA { short item; long qty; }; IMPLEMENT_DYNCREATE(SalesView, CDaoRecordView) /***************************************************************************/ /* */ /* SalesView Message Map */ /* */ /***************************************************************************/ BEGIN_MESSAGE_MAP(SalesView, CDaoRecordView) ON_COMMAND(CM_ADDRECORD, OnAddRecord) ON_COMMAND(CM_PASTENEW, OnPasteNewRecord) ... ON_COMMAND(ID_EDIT_COPY, OnCopyRecord) ON_COMMAND(ID_EDIT_CUT, OnDeleteRecord) ON_COMMAND(ID_EDIT_PASTE, OnPasteOver) ... ON_COMMAND(CM_REFRESH, OnRefreshRecord) ON_UPDATE_COMMAND_UI(ID_EDIT_PASTE, OnUpdatePasteOver) ON_UPDATE_COMMAND_UI(CM_PASTENEW, OnUpdatePasteNew) ON_UPDATE_COMMAND_UI(CM_ADDRECORD, OnUpdateAddRecord) ON_UPDATE_COMMAND_UI(ID_EDIT_CUT, OnUpdateDeleteRecord) END_MESSAGE_MAP() /***************************************************************************/ /* */ /* SalesView: constructor initializes, Opens the database main Sales table */ /* */ /***************************************************************************/ SalesView::SalesView () : CDaoRecordView(SalesView::IDD) { Windows MFC Programming II m_pSet = NULL; m_pSet = new DAOSales (); m_pSet->Open (); add_mode = FALSE; ... 472 // allocate a new Sales Table record set // and open it; it then positions to first record // set for no ongoing add record operation } /***************************************************************************/ /* */ /* ~SalesData: destroy view by removing DB record set and any calc arrays */ /* */ /***************************************************************************/ SalesView::~SalesView () { if (calcs_setup) FreeCalculations (); m_pSet->Close (); delete m_pSet; } /***************************************************************************/ /* */ /* DoDataExchange: xfer data to/from the record set and our controls */ /* */ /***************************************************************************/ void SalesView::DoDataExchange (CDataExchange *pDX) { CDaoRecordView::DoDataExchange(pDX); DDX_FieldText(pDX, IDC_ITEMNUMBER, m_pSet->m_Item_Number, m_pSet); DDX_FieldText(pDX, IDC_QUANTITY, m_pSet->m_Quantity_Sold, m_pSet); } /***************************************************************************/ /* */ /* OnInitialUpdate: Set the dialog controls to read only in DB cannot updt */ /* */ /***************************************************************************/ void SalesView::OnInitialUpdate () { CDaoRecordView::OnInitialUpdate(); // set controls to read-only if the DB cannot handle updates if (!m_pSet->CanUpdate ()) { ((CEdit*) GetDlgItem (IDC_ITEMNUMBER))->SetReadOnly (TRUE); ((CEdit*) GetDlgItem (IDC_QUANTITY))->SetReadOnly (TRUE); } } /***************************************************************************/ /* */ /* OnGetRecordset: obtain the DAO record set pointer */ /* */ /***************************************************************************/ CDaoRecordset* SalesView::OnGetRecordset () { return m_pSet; } ... /***************************************************************************/ /* */ /* OnAddRecord: begin add new record process - OnMove finishes the add or */ Windows MFC Programming II 473 /* OnRefreshRecord cancels the add process */ /* */ /***************************************************************************/ void SalesView::OnAddRecord () { if (m_pSet->CanAppend ()) { // can DB handle an add record? if (add_mode) // is any previous add not completed? OnMove (ID_RECORD_FIRST); // yes, go finish the previous add m_pSet->AddNew (); // insert new record at end of DB add_mode = TRUE; // indicate add in progress UpdateData (FALSE); // force dialog's controls to be cleared } } /***************************************************************************/ /* */ /* OnMove: complete any previous add request - then handle move request */ /* */ /***************************************************************************/ BOOL SalesView::OnMove (UINT nIDMoveCommand) { if (add_mode) { // is there an on-going add request? if (!UpdateData ()) return FALSE; // yes, xfer controls to DB fields try { m_pSet->Update (); // force that record to be updated } catch (CDaoException *ptrex) { // failed, show why AfxMessageBox (ptrex->m_pErrorInfo->m_strDescription); ptrex->Delete (); } add_mode = FALSE; // indicate ass is now done m_pSet->Requery (); // issue refresh the record set UpdateData (); // and xfer new data from dialog controls } return CDaoRecordView::OnMove(nIDMoveCommand); } /***************************************************************************/ /* */ /* OnDeleteRecord: remove current record from the DB */ /* */ /***************************************************************************/ void SalesView::OnDeleteRecord () { if (m_pSet->CanUpdate ()) { // are deletes supported? try { m_pSet->Delete (); // yes, delete this record } catch (CDaoException *ptrex) { // failed, show why AfxMessageBox (ptrex->m_pErrorInfo->m_strDescription); ptrex->Delete (); } m_pSet->MoveNext (); // attempt to move to next if (m_pSet->IsEOF ()) m_pSet->MoveLast (); // failed, so move to last if (m_pSet->IsBOF ()) m_pSet->SetFieldNull (NULL);// failed, no records inDB UpdateData (FALSE); // put cur rec into dialog } } Windows MFC Programming II 474 /***************************************************************************/ /* */ /* OnCopyRecord: copy the current record to the clipboard */ /* */ /***************************************************************************/ void SalesView::OnCopyRecord () { // obtain global memory for our data to give to Clipboard HANDLE hbuffer = GlobalAlloc (GMEM_MOVEABLE, sizeof (OURCLIPDATA)); if (hbuffer==NULL) return; // copy current record to the global copy OURCLIPDATA *ptrbuf = (OURCLIPDATA*) GlobalLock (hbuffer); ptrbuf->item = m_pSet->m_Item_Number; ptrbuf->qty = m_pSet->m_Quantity_Sold; // transfer global copy to the Clipboard VERIFY(OpenClipboard ()); VERIFY(::EmptyClipboard ()); VERIFY(::SetClipboardData (CF_PRIVATEFIRST, hbuffer)); VERIFY(::CloseClipboard ()); } /***************************************************************************/ /* */ /* OnPasteNewRecord: Paste Clipboard copy into a new record in the DB */ /* */ /***************************************************************************/ void SalesView::OnPasteNewRecord () { // verify DB can add and that there is data on the Clipboard to paste if (m_pSet->CanAppend () && IsClipboardFormatAvailable (CF_PRIVATEFIRST)) { // retrieve the data from the Clipboard HANDLE hbuffer; VERIFY(OpenClipboard ()); VERIFY(hbuffer = GetClipboardData (CF_PRIVATEFIRST)); VERIFY(::CloseClipboard ()); OURCLIPDATA *ptrbuf = (OURCLIPDATA*) GlobalLock (hbuffer); if (add_mode) OnMove (ID_RECORD_FIRST); // complete any add in progress m_pSet->AddNew (); // make a new record for our paste add_mode = TRUE; // enable add mode operations OnMove (ID_RECORD_FIRST); // install the null record via OnMove OnMove (ID_RECORD_LAST); // position to the new record try { m_pSet->Edit (); // enable DB's Edit mode m_pSet->m_Item_Number = ptrbuf->item; // update the record from Clipboard m_pSet->m_Quantity_Sold = ptrbuf->qty; m_pSet->Update (); // force record to be updated } catch (CDaoException *ptrex) { // failed, show why AfxMessageBox (ptrex->m_pErrorInfo->m_strDescription); ptrex->Delete (); } GlobalUnlock (hbuffer); // pitch the Clipboard data GlobalFree (hbuffer); UpdateData (FALSE); // force update of dialog controls Windows MFC Programming II 475 } } /***************************************************************************/ /* */ /* OnPasteOver: Paste Clipboard data over this existing record */ /* */ /***************************************************************************/ void SalesView::OnPasteOver () { // verify DB can update and data is on the Clipboard if (m_pSet->CanUpdate () && IsClipboardFormatAvailable (CF_PRIVATEFIRST)) { // get the record from the Clipboard HANDLE hbuffer; VERIFY(OpenClipboard ()); VERIFY(hbuffer = GetClipboardData (CF_PRIVATEFIRST)); VERIFY(::CloseClipboard ()); OURCLIPDATA *ptrbuf = (OURCLIPDATA*) GlobalLock (hbuffer); try { m_pSet->Edit (); // enter Edit mode m_pSet->m_Item_Number = ptrbuf->item; // change this record's data m_pSet->m_Quantity_Sold = ptrbuf->qty; m_pSet->Update (); // complete DB update } catch (CDaoException *ptrex) { // failed, show why AfxMessageBox (ptrex->m_pErrorInfo->m_strDescription); ptrex->Delete (); } UpdateData (FALSE); // force update of dialog controls } } /***************************************************************************/ /* */ /* OnRefreshRecord: cancel any add mode request - refresh dialog controls */ /* */ /***************************************************************************/ void SalesView::OnRefreshRecord () { if (add_mode) { // is any add record in progress? m_pSet->CancelUpdate (); // yes, cancel that request m_pSet->Move (0); // force reposition current record ptr add_mode = FALSE; // turn off add mode } UpdateData (FALSE); // all cases, refresh dialog controls } /***************************************************************************/ /* */ /* Command Enablers for: OnAddRecord, OnDeleteRecord, two Paste functions */ /* */ /***************************************************************************/ void SalesView::OnUpdatePasteOver (CCmdUI *pCmdUI) { pCmdUI->Enable (m_pSet->CanUpdate () && IsClipboardFormatAvailable (CF_PRIVATEFIRST)); } Windows MFC Programming II 476 void SalesView::OnUpdatePasteNew (CCmdUI *pCmdUI) { pCmdUI->Enable (m_pSet->CanUpdate () && IsClipboardFormatAvailable (CF_PRIVATEFIRST)); } void SalesView::OnUpdateAddRecord (CCmdUI *pCmdUI) { pCmdUI->Enable (m_pSet->CanUpdate () && m_pSet->CanAppend ()); } void SalesView::OnUpdateDeleteRecord (CCmdUI *pCmdUI) { pCmdUI->Enable (m_pSet->CanUpdate ()); } ... /***************************************************************************/ /* */ /* Dubugging Only Function */ /* */ /***************************************************************************/ #ifdef _DEBUG ... /***************************************************************************/ /* */ /* GetDocument: debugging version - prod version is inline */ /* */ /***************************************************************************/ SalesDoc* SalesView::GetDocument () { // non-debug version is inline ASSERT (m_pDocument->IsKindOf (RUNTIME_CLASS (SalesDoc))); return (SalesDoc*) m_pDocument; } #endif //_DEBUG Notice that there is no function called OnEdit. Rather the user can at once alter the contents of the dialog controls. When are these changes retrieved and passed on to the record set and the database? Whenever the user moves to another record. In OnMove which responds to all move record requests, if additions are not being made, control goes straight to the base class which automatically transfers the data for us. Deletions present the additional problem of what record to display in the view after the current record is deleted. The usual choice is the next record in the set; if none, the previous record. In OnDeleteRecord, if the database can handle deletions, invoke the CDaoRecordSet’s Delete function; the current record is deleted. Notice that I placed the potentially troublesome request in a try-catch block. If there is an error, a CDaoException is thrown and the appropriate message is displayed. If all goes well, I then try to reposition the current record ... m_pSet->Delete (); ... m_pSet->MoveNext (); if (m_pSet->IsEOF ()) m_pSet->MoveLast (); if (m_pSet->IsBOF ()) m_pSet->SetFieldNull (NULL); UpdateData (FALSE); Windows MFC Programming II 477 UpdateDate, a CWnd function, when passed FALSE, transfers the current database record in the record set into the dialog controls. Adding records is trickier because the added record is placed at the end of the record set and is not committed until the next move record operation. In OnAddRecord, the process is begun. CanAppend returns TRUE if the database is able to support the add records option. However, the user could still be in the middle of an add record operation. That is, the user could select add record and then before moving from that new record, select add record once more. Therefore, the first action is to check the add_mode BOOL. If it is TRUE, then OnMove is invoked directly to force that addition to be completed before beginning a new one. The AddNew function prepares a new record in the record set, appended to the end. UpdateData then reloads the dialog controls with that data; add_mode is set to TRUE if (m_pSet->CanAppend ()) { if (add_mode) OnMove (ID_RECORD_FIRST); m_pSet->AddNew (); add_mode = TRUE; UpdateData (FALSE); } When OnMove is called in response to an move to another record, if add_mode is TRUE, the new data must be saved. UpdateData is called (using TRUE which is the default) to transfer the data from the controls into the record set. Then the record set’s Update function is attempted. Again I wrap a try-catch block around the database activity, just in case. Once complete, the Requery function forces the record set to reload all records including the newly added one if (add_mode) { if (!UpdateData ()) return FALSE; ... m_pSet->Update (); ... add_mode = FALSE; m_pSet->Requery (); UpdateData (); } return CDaoRecordView::OnMove(nIDMoveCommand); To use the Clipboard for cut/paste operations, the item number and quantity must be somehow transferred. While we could convert the data into a text stream and use CF_TEXT, in this example, placing our own data type onto the Clipboard is the most straightforward. I define a OURCLIPDATA structure to hold the short and long. To copy the current record to the Clipboard, construct a global memory instance of OURCLIPDATA, fill it with the current data, and give it to the Clipboard using the CF_PRIVATEFIRST data type as shown in OnCopyRecord HANDLE hbuffer = GlobalAlloc (GMEM_MOVEABLE, sizeof (OURCLIPDATA)); if (hbuffer==NULL) return; OURCLIPDATA *ptrbuf = (OURCLIPDATA*) GlobalLock (hbuffer); Windows MFC Programming II 478 ptrbuf->item = m_pSet->m_Item_Number; ptrbuf->qty = m_pSet->m_Quantity_Sold; VERIFY(OpenClipboard ()); VERIFY(::EmptyClipboard ()); VERIFY(::SetClipboardData (CF_PRIVATEFIRST, hbuffer)); VERIFY(::CloseClipboard ()); To paste over the current record, obtain a global handle of the Clipboard data, invoke the Edit record set function, copy the data into the record set members and attempt to Update it. Again, I wrapped a try-catch block around the potentially troublesome record set operations if (m_pSet->CanUpdate () && IsClipboardFormatAvailable (CF_PRIVATEFIRST)) { HANDLE hbuffer; VERIFY(OpenClipboard ()); VERIFY(hbuffer = GetClipboardData (CF_PRIVATEFIRST)); VERIFY(::CloseClipboard ()); OURCLIPDATA *ptrbuf = (OURCLIPDATA*) GlobalLock (hbuffer); ... m_pSet->Edit (); m_pSet->m_Item_Number = ptrbuf->item; m_pSet->m_Quantity_Sold = ptrbuf->qty; m_pSet->Update (); ... UpdateData (FALSE); To paste to a new record, first add a new record, appended to the end of the record set. By moving to the first record and then to the last record, I have forced that record to be added and become the current record. Now repeat the paste over actions, using the Edit and Update functions. The command enablers check for the database capabilities as well as for CF_PRIVATEFIRST data present on the Clipboard. The document class, SalesDoc, owns the single instance of the CDaoDatabase instance with a member function, GetDB, to return a pointer to that instance. The recordset Open functions are passed this pointer. Windows MFC Programming II 479 Accumulating the Total Sales By Category and Item — Using Five Record Sets — Dynamic Allocation of Two-dimensional Arrays The discussion here is the same as for ODBC. Listing for File: SalesView.cpp — Part 2 — Pgm08b /***************************************************************************/ /* */ /* SalesView: constructor initializes, Opens the database main Sales table */ /* */ /***************************************************************************/ SalesView::SalesView () : CDaoRecordView(SalesView::IDD) { ... add_mode = FALSE; // set for no ongoing add record operation print_done = FALSE; // when printing, tot pages = tot_cats // initialize to NULL the calc / printing members, counts, 1D, and 2D arrays calcs_setup = FALSE;// when TRUE, these are allocated tot_cats = 0; // total number of categories tot_items_per_cat = NULL; // total items per category tot_items = 0; // the total number of items itemnum_to_itemidx = NULL; // array of itemnums to their 2D table indices unitcost = NULL; // the unit costs of each item cats = NULL; // the category number of eac item categories = NULL; // the category string names max_item_idx = 0; // max number of items in all categories itemidx_to_itemnum = NULL; // conversion to real item numbers sales = NULL; // 2D table of sales } /***************************************************************************/ /* */ /* ~SalesData: destroy view by removing DB record set and any calc arrays */ /* */ /***************************************************************************/ SalesView::~SalesView () { if (calcs_setup) FreeCalculations (); ... } /***************************************************************************/ /* */ /* DoCalculations: alloc single and 2D arrays, fills them, calcs sales */ /* */ /***************************************************************************/ // assumes the Categories Table is sorted ito category number order // assumes the Items Table is sorted by category and by Item Number // and that item and category numbers are sequential from 0 // assumes a minimum of database activity to accumulate the sales BOOL SalesView::DoCalculations () { if (calcs_setup) return TRUE; // avoid recursion Windows MFC Programming II m_pSet->Requery (); 480 // get latest changes, if any int i, j; tot_cats = 0; tot_items = 0; max_item_idx = 0; // total number of categories // the total number of items // max number of items in all categories // set up the main database object CDaoDatabase *ptrdb = GetDocument ()->GetDB (); if (!ptrdb->IsOpen ()) return FALSE; BOOL ok = TRUE; // setup and open the required tables the two queries and the Items Table DAONumCat dbnumcat (ptrdb); DAOGetCat dbcat (ptrdb); DAONumItem dbnumitem (ptrdb); DAOGetItem dbitems (ptrdb); try { dbnumcat.Open (); dbcat.Open (); dbnumitem.Open (); dbitems.Open (); } catch (CDaoException *ptrex) { AfxMessageBox (ptrex->m_pErrorInfo->m_strDescription); ptrex->Delete (); ok = FALSE; } if (!ok) return FALSE; // get the total number of categories tot_cats = dbnumcat.m_CountOfCategory_Number; // allocate the number items in each category table tot_items_per_cat = new int [tot_cats]; tot_items = 0; // the total number of items // go get the number of items per category for (i=0; i<tot_cats; i++) { j = dbnumitem.m_Category_Number; tot_items_per_cat[j] = dbnumitem.m_CountOfItem_Number; tot_items += tot_items_per_cat[j]; dbnumitem.MoveNext (); } dbnumitem.Close (); dbnumcat.Close (); // allocate and load the categories string array categories = new CString [tot_cats]; for (i=0; i<tot_cats; i++) { categories[i] = dbcat.m_Category; dbcat.MoveNext (); } dbcat.Close (); // allocate the conversion array of item numbers to their 2D table indexes Windows MFC Programming II 481 // and the unit costs of each item and its category number itemnum_to_itemidx = new int [tot_items]; unitcost = new double [tot_items]; cats = new BYTE [tot_items]; // get the maximum number of items of all categories for 2D table bounds max_item_idx = 0; for (i=0; i<tot_cats; i++) if (max_item_idx < tot_items_per_cat[i]) max_item_idx = tot_items_per_cat[i]; // allocate the 2D tables for // sales[max cats][max items] the total item sales // itemidx_to_itemnum [max cats][max items] conversion to real item number sales = new double* [tot_cats]; itemidx_to_itemnum = new short* [tot_cats]; for (i=0; i<tot_cats; i++) { sales[i] = new double [max_item_idx]; itemidx_to_itemnum[i] = new short [max_item_idx]; } int idx = 0; BYTE oldcat = dbitems.m_Category_Number; BYTE cat; // setup the item number to 2D table index conversions and the reverse // along with the unitcost of each item and its category number while (!dbitems.IsEOF ()) { cat = dbitems.m_Category_Number; if (oldcat != cat) { oldcat = cat; idx = 0; } itemidx_to_itemnum[cat][idx] = dbitems.m_Item_Number; itemnum_to_itemidx[dbitems.m_Item_Number] = idx; unitcost[dbitems.m_Item_Number] = dbitems.m_Unit_Cost; cats[dbitems.m_Item_Number] = cat; idx++; dbitems.MoveNext (); } dbitems.Close (); // clear sales array for (i=0; i<tot_cats; i++) for (j=0; j<max_item_idx; j++) sales[i][j] = 0.; // accumulate the total sales by category and item number m_pSet->MoveFirst (); while (!m_pSet->IsEOF ()) { short item = m_pSet->m_Item_Number; cat = cats[item]; idx = itemnum_to_itemidx[item]; sales [cat][idx] += unitcost[item] * m_pSet->m_Quantity_Sold; m_pSet->MoveNext (); } calcs_setup = TRUE; return TRUE; Windows MFC Programming II 482 } /***************************************************************************/ /* */ /* FreeCalculations: remove all of the calculation arrays */ /* */ /***************************************************************************/ void SalesView::FreeCalculations () { if (!calcs_setup) return; // nothing to do int i; // free the two 2D arrays if (sales) { for (i=0; i<tot_cats; i++) delete [] sales[i]; delete [] sales; sales = NULL; } if (itemidx_to_itemnum) { for (i=0; i<tot_cats; i++) delete [] itemidx_to_itemnum[i]; delete [] itemidx_to_itemnum; itemidx_to_itemnum = NULL; } // free all 1D arrays if (tot_items_per_cat) { delete [] tot_items_per_cat; tot_items_per_cat = NULL; } if (itemnum_to_itemidx) { delete [] itemnum_to_itemidx; itemnum_to_itemidx = NULL; } if (unitcost) { delete [] unitcost; unitcost = NULL; } if (cats) { delete [] cats; cats = NULL; } if (categories) { delete [] categories; categories = NULL; } calcs_setup = FALSE; } ... The first action is to Requery the Sales Table to force all pending changes to be implemented. A pointer to the CDaoDatabase from the document is retrieved so that the other record sets can be allocated. Again I placed the Open calls within a try-catch block. m_pSet->Requery (); ... CDaoDatabase *ptrdb = GetDocument ()->GetDB (); if (!ptrdb->IsOpen ()) return FALSE; Windows MFC Programming II 483 BOOL ok = TRUE; DAONumCat dbnumcat (ptrdb); DAOGetCat dbcat (ptrdb); DAONumItem dbnumitem (ptrdb); DAOGetItem dbitems (ptrdb); try { dbnumcat.Open (); dbcat.Open (); dbnumitem.Open (); dbitems.Open (); } catch (CDaoException *ptrex) { AfxMessageBox (ptrex->m_pErrorInfo->m_strDescription); ptrex->Delete (); ok = FALSE; } if (!ok) return FALSE; The tot_cats is retrieved from the DAONumCat record set. Similarly the array tot_items_per_cat is allocated and the DAONumItem query table is input record by record, here just three categories are present. tot_cats = dbnumcat.m_CountOfCategory_Number; tot_items_per_cat = new int [tot_cats]; tot_items = 0; // the total number of items for (i=0; i<tot_cats; i++) { j = dbnumitem.m_Category_Number; tot_items_per_cat[j] = dbnumitem.m_CountOfItem_Number; tot_items += tot_items_per_cat[j]; dbnumitem.MoveNext (); } dbnumitem.Close (); dbnumcat.Close (); For convenience, I allocate an array of CStrings to hold the three category names. categories = new CString [tot_cats]; for (i=0; i<tot_cats; i++) { categories[i] = dbcat.m_Category; dbcat.MoveNext (); } dbcat.Close (); Then allocate the conversion array of item numbers to their 2D table indexes and the unit costs of each item and its category number, all based on the tot_items. In order to allocate the two-dimensional tables, I must get the maximum number of items of all categories. itemnum_to_itemidx = new int [tot_items]; unitcost = new double [tot_items]; cats = new BYTE [tot_items]; max_item_idx = 0; for (i=0; i<tot_cats; i++) Windows MFC Programming II 484 if (max_item_idx < tot_items_per_cat[i]) max_item_idx = tot_items_per_cat[i]; Now the two two-dimensional tables can be allocated similar to sales[max cats][max items] and itemidx_to_itemnum [max cats][max items]. sales = new double* [tot_cats]; itemidx_to_itemnum = new short* [tot_cats]; for (i=0; i<tot_cats; i++) { sales[i] = new double [max_item_idx]; itemidx_to_itemnum[i] = new short [max_item_idx]; } The conversion tables of item numbers to index and index to item number must be calculated. DAOGetItem provides the category for reach item number along with the unit cost. So while I am building the conversion arrays, I might as well store the unit costs as well. The assumption is that the Items Table is sorted by category and then by item number. A control break occurs whenever the new record’s category number does not equal the previous record’s category number. At such a point, it is time for a new column; reset the current index, idx, back to 0 for the new column. int idx = 0; BYTE oldcat = dbitems.m_Category_Number; BYTE cat; while (!dbitems.IsEOF ()) { cat = dbitems.m_Category_Number; if (oldcat != cat) { oldcat = cat; idx = 0; } itemidx_to_itemnum[cat][idx] = dbitems.m_Item_Number; itemnum_to_itemidx[dbitems.m_Item_Number] = idx; unitcost[dbitems.m_Item_Number] = dbitems.m_Unit_Cost; cats[dbitems.m_Item_Number] = cat; idx++; dbitems.MoveNext (); } dbitems.Close (); After clearing the sales array of doubles, the accumulation of the total sales can begin. MoveFirst positions the Sales table back to the beginning. The loop continues until EOF is detected. From the current record’s item number, the corresponding category number and column index are found from the arrays and then used to access sales and the unit cost arrays. m_pSet->MoveFirst (); while (!m_pSet->IsEOF ()) { short item = m_pSet->m_Item_Number; cat = cats[item]; idx = itemnum_to_itemidx[item]; sales [cat][idx] += unitcost[item] * m_pSet->m_Quantity_Sold; m_pSet->MoveNext (); } Windows MFC Programming II 485 calcs_setup = TRUE; The Printing Operations of Pgm08b Listing for File: SalesView.cpp — Part 3 — Pgm08b ... /***************************************************************************/ /* */ /* SalesView Message Map */ /* */ /***************************************************************************/ BEGIN_MESSAGE_MAP(SalesView, CDaoRecordView) ... ON_COMMAND(CM_QUICKVIEW, OnQuickView) ON_COMMAND(CM_CLEARQUICKVIEW, OnClearQuickView) ... ON_COMMAND(ID_FILE_PRINT, OnFilePrint) ON_COMMAND(ID_FILE_PRINT_DIRECT, OnFilePrint) ON_COMMAND(ID_FILE_PRINT_PREVIEW, OnFilePrintPreview) ... END_MESSAGE_MAP() ... /***************************************************************************/ /* */ /* OnFilePrint: print the sales report */ /* */ /***************************************************************************/ void SalesView::OnFilePrint () { // force printer into landscape mode Pgm08bApp *ptrapp = (Pgm08bApp*) AfxGetApp (); if (ptrapp) ptrapp->SetLandscape (); // note, the user could still set it back in the Print Dlg if desired if (calcs_setup) FreeCalculations (); // remove existing calcs, if any if (!DoCalculations ()) { // attempt to calc the sales FreeCalculations (); // failed, remove any arrays left return; } CDaoRecordView::OnFilePrint (); // go print the report if (calcs_setup) FreeCalculations (); // remove arrays } /***************************************************************************/ /* */ /* OnPreparePrinting: set total pages and preview pages */ /* */ /***************************************************************************/ BOOL SalesView::OnPreparePrinting (CPrintInfo *pInfo) { if (pInfo->m_bPreview) { // for print preview pInfo->m_nNumPreviewPages = tot_cats; // give guess for total pages Windows MFC Programming II pInfo->SetMaxPage (1); } pInfo->SetMaxPage (tot_cats); 486 // set max for one landscape page // for print, set total pages return DoPreparePrinting(pInfo); } /***************************************************************************/ /* */ /* OnPrint: print the current page of the report */ /* */ /***************************************************************************/ void SalesView::OnPrint(CDC *pDC, CPrintInfo *pInfo) { int pagenum = pInfo->m_nCurPage; // retrieve the page to be rendered if (pagenum -1 > tot_cats) { // check for end of report print_done = TRUE; return; } OnPrepareDC (pDC, pInfo); // fix up the DC for rendering // construct the display page rectangle and convert it into logical coords CRect r; r.left = r.top = 0; r.right = pDC->GetDeviceCaps (HORZRES); r.bottom = pDC->GetDeviceCaps (VERTRES); pDC->DPtoLP ((CPoint*) &r, 2); RenderPage (pDC, pagenum, r); // go print this page } /***************************************************************************/ /* */ /* OnFilePrintPreview: preview the printed report */ /* */ /***************************************************************************/ void SalesView::OnFilePrintPreview () { // force the printer into landscape mode Pgm08bApp *ptrapp = (Pgm08bApp*) AfxGetApp (); if (ptrapp) ptrapp->SetLandscape (); if (calcs_setup) FreeCalculations (); // remove previous calcs, if any if (!DoCalculations ()) { // attempt to calc the sales FreeCalculations (); // failed, remove any arrays left return; } CDaoRecordView::OnFilePrintPreview (); // note the preview thread returns here long before preview is really done // so the free array problem is handled by always using calcs_setup BOOL and // freeing the array it it already exists before each new action } /***************************************************************************/ /* */ /* OnPrepareDC: set the continue printing flag - scale print/preview DCs */ /* */ Windows MFC Programming II 487 /***************************************************************************/ void SalesView::OnPrepareDC (CDC *pDC, CPrintInfo *pInfo) { CDaoRecordView::OnPrepareDC(pDC, pInfo); // separate printing from display cases and preview from print if (pInfo != NULL) { // here we are printing // set the continue printing flag - if previewing, always true if (pInfo->m_bPreview) pInfo->m_bContinuePrinting = TRUE; // but if printing, controlled by pages actually printed else if (!print_done) pInfo->m_bContinuePrinting = TRUE; else pInfo->m_bContinuePrinting = FALSE; } if (pDC->IsPrinting ()) { // set screen size to that of a full screen CRect clrect (0, 0, GetSystemMetrics (SM_CXSCREEN), GetSystemMetrics (SM_CYSCREEN)); CSize clsize = clrect.Size(); // set the size of the screen for window ext // set the printer page size for viewport extent CSize pagesize = CSize (pDC->GetDeviceCaps (HORZRES), pDC->GetDeviceCaps (VERTRES)); // install mode and scaling effects pDC->SetMapMode (MM_ANISOTROPIC); pDC->SetWindowExt (clsize); pDC->SetViewportExt (pagesize); } } /***************************************************************************/ /* */ /* RenderPage: renders one report page on the passed DC */ /* */ /***************************************************************************/ void SalesView::RenderPage (CDC *ptrdc, int pagenum, CRect rfull) { // display the heading across the page DisplayHeading (ptrdc, rfull, categories[pagenum - 1], pagenum); // calc the CRect rtext rtext.right DisplayText display area for column view and show that portion = rfull; = rtext.left + rtext.Width () / 3; (ptrdc, rtext, pagenum - 1); // calc the display area for the graph and show graph CRect rdraw = rfull; rdraw.left = rtext.right; DisplayGraph (ptrdc, rdraw, pagenum - 1); } /***************************************************************************/ /* */ /* OnQuickView: Show the first page of the report for inspection */ /* */ /***************************************************************************/ void SalesView::OnQuickView () { if (calcs_setup) FreeCalculations (); // remove arrays if they still exist Windows MFC Programming II 488 if (!DoCalculations ()) { FreeCalculations (); return; } // attempt to calc the sales // failed, remove any arrays left // and abort QuickView CClientDC dc (this); CRect r; GetClientRect (&r); r.top += 50; // // // // BYTE which_cat = 0; RenderPage (&dc, which_cat + 1, r); // you can adjust which page is viewed // render the one page on-screen FreeCalculations (); // remove calc arrays make a DC to view upon setup a display rectangle that does not interfeer with dialog controls } /***************************************************************************/ /* */ /* DisplayHeading: display the main report heading across the page top */ /* */ /***************************************************************************/ void SalesView::DisplayHeading (CDC *ptrdc, CRect &r, CString &category, int page) { ptrdc->SetBkMode (TRANSPARENT); ptrdc->SetBkColor (RGB (192, 192, 192)); TEXTMETRIC tm; // make to rather large fonts for heading and page number and category CFont fontbig; fontbig.CreateFont (36, 0, 0, 0, FW_BOLD, 0, 0, 0, DEFAULT_CHARSET, OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS, PROOF_QUALITY, FF_ROMAN | TMPF_TRUETYPE, "Times New Roman"); CFont fontmed; fontmed.CreateFont (24, 0, 0, 0, FW_BOLD, 0, 0, 0, DEFAULT_CHARSET, OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS, PROOF_QUALITY, FF_ROMAN | TMPF_TRUETYPE, "Times New Roman"); // calculate big font's average character dimensions ptrdc->SelectObject (&fontbig); ptrdc->GetTextMetrics (&tm); avg_char_width = tm.tmAveCharWidth; avg_char_height = tm.tmHeight + tm.tmExternalLeading; // display main header CRect rtop = r; rtop.bottom = r.top + avg_char_height; CString head ("Acme Sales by Category"); ptrdc->DrawText (head, &rtop, DT_CENTER); r.top += avg_char_height * 2; // set up page number and its location rectangle-wait for med font to show it char m[30]; wsprintf (m,"Page: %d", page); CRect rpage = rtop; rpage.OffsetRect (0, avg_char_height); // select medium font and calculate its average character dimensions Windows MFC Programming II 489 ptrdc->SelectObject (&fontmed); ptrdc->GetTextMetrics (&tm); avg_char_width = tm.tmAveCharWidth; avg_char_height = tm.tmHeight + tm.tmExternalLeading; // show page number ptrdc->DrawText (m, &rpage, DT_CENTER); // show category line with gray stripe rtop = r; rtop.bottom = r.top + avg_char_height; CBrush br; br.CreateSolidBrush (RGB (200, 200, 200)); CBrush *ptroldbr = ptrdc->SelectObject (&br); CPen *ptroldpen = (CPen*) ptrdc->SelectStockObject (NULL_PEN); rtop.top -= 4; // since text is at very top, add a spacer for aestetics // show stripe ptrdc->Rectangle (&rtop); ptrdc->SelectObject (ptroldbr); ptrdc->SelectObject (ptroldpen); rtop.top += 4; // show category line over the top of the stripe ptrdc->TextOut (rtop.left, rtop.top, category); r.top += avg_char_height * 2; } /***************************************************************************/ /* */ /* DisplayText: render the text columnar listing on the left 1/3 of a page */ /* */ /***************************************************************************/ void SalesView::DisplayText (CDC *ptrdc, CRect &r, BYTE cat) { TEXTMETRIC tm; // make a bold and a regular smaller font CFont fontbold, fontnorm; fontbold.CreateFont (20, 0, 0, 0, FW_BOLD, 0, 0, 0, DEFAULT_CHARSET, OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS, PROOF_QUALITY, FF_ROMAN | TMPF_TRUETYPE, "Times New Roman"); fontnorm.CreateFont (20, 0, 0, 0, FW_NORMAL, 0, 0, 0, DEFAULT_CHARSET, OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS, PROOF_QUALITY, FF_ROMAN | TMPF_TRUETYPE, "Times New Roman"); ptrdc->SelectObject (&fontbold); ptrdc->GetTextMetrics (&tm); avg_char_width = tm.tmAveCharWidth; avg_char_height = tm.tmHeight + tm.tmExternalLeading; // rect r holds whole left 1/3 area // make rprod and rsls rects for the two columns of data CRect rsls = r; CRect rprod = r; rprod.right = rprod.right - avg_char_width * 11; rsls.left = rsls.right - avg_char_width * 11; rprod.bottom = rprod.top + avg_char_height; rsls.bottom = rsls.top + avg_char_height; Windows MFC Programming II 490 // display the two column headings CString prod ("Product:"); ptrdc->TextOut (rprod.left, rprod.top, prod); rprod.OffsetRect (0, avg_char_height); CString sls ("Sales:"); ptrdc->TextOut (rsls.left, rsls.top, sls); rsls.OffsetRect (0, avg_char_height); // now underline the whole column heading line ptrdc->PatBlt (r.left, r.top + avg_char_height - 6, r.Width (), 2, BLACKNESS); // select the main listing font ptrdc->SelectObject (&fontnorm); // input all items in this category from the DB and print their names & sales // begin by getting a pointer to the database itself and then an instance of // the Items Table itself in which to look up the actual item name CDaoDatabase *ptrdb = GetDocument ()->GetDB (); if (!ptrdb->IsOpen ()) return; BOOL ok = TRUE; // setup and open the required tables the two queries and the Items Table DAOGetItem dbitems (ptrdb); try { dbitems.Open (); } catch (CDaoException *ptrex) { AfxMessageBox (ptrex->m_pErrorInfo->m_strDescription); ptrex->Delete (); ok = FALSE; } if (!ok) return; // for direct queries, must open table with dbOpenTable and then indicate // which index is to be used in the queries dbitems.SetCurrentIndex (_T("PrimaryKey")); int i; short item; // for all items in this category, look up its name and print name and sales for (i=0; i<tot_items_per_cat[cat]; i++) { // convert item index back to item number in db item = itemidx_to_itemnum[cat][i]; // set table to that item number dbitems.Seek (_T("="), &COleVariant (item, VT_I2)); // display the item name ptrdc->TextOut (rprod.left, rprod.top, dbitems.m_Item_Name); rprod.OffsetRect (0, avg_char_height); // format and display the sales, right justified char s[15]; sprintf (s,"%10.2lf",sales[cat][i]); ptrdc->TextOut (rsls.left, rsls.top, "$"); ptrdc->SetTextAlign (TA_RIGHT); ptrdc->TextOut (rsls.right, rsls.top, s); ptrdc->SetTextAlign (TA_LEFT); rsls.OffsetRect (0, avg_char_height); } Windows MFC Programming II 491 dbitems.Close (); } /***************************************************************************/ /* */ /* DisplayGraph: render the bar chart on the right 2/3 of the page */ /* */ /***************************************************************************/ void SalesView::DisplayGraph (CDC *ptrdc, CRect &r, BYTE cat) { TEXTMETRIC tm; // make very small font CFont font; font.CreateFont (14, 0, 0, 0, FW_NORMAL, 0, 0, 0, DEFAULT_CHARSET, OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS, PROOF_QUALITY, FF_ROMAN | TMPF_TRUETYPE, "Times New Roman"); ptrdc->SelectObject (&font); ptrdc->GetTextMetrics (&tm); avg_char_width = tm.tmAveCharWidth; avg_char_height = tm.tmHeight + tm.tmExternalLeading; const const const const const int int int int int bar_height = 20; max_sls = 2000; big_ticks = 5; sml_ticks = 3; sml_tick_height = 5; // // // // // uniform height of all bars the maximum sales graphed number of vertical main lines num tiny ticks between the main lines tiny tick height // setup the product id side to the left of the chart and the graph rects CRect rprod = r; CRect rgraf = r; rprod.right = rprod.left + avg_char_width * 25; rprod.top += bar_height / 2; rprod.bottom = rprod.top + avg_char_height; rgraf.left = rprod.right + avg_char_width; rgraf.right -= avg_char_width * 3; int num = tot_items_per_cat[cat]; rgraf.bottom = rgraf.top + num * bar_height * 2; int w = rgraf.Width () / big_ticks; int ws = w / (sml_ticks + 1); // now draw the graph grid of vertical lines // and the ticks across the bottom in between the main vertical lines // and display the horizontal scale values below each main vertical line int i, j; int x, y; for (i=0; i<big_ticks+1; i++) { x = rgraf.left + i * w; for (j=0; j<sml_ticks; j++) { ptrdc->MoveTo (x + (j+1) * ws, rgraf.bottom); ptrdc->LineTo (x + (j+1) * ws, rgraf.bottom + sml_tick_height); } ptrdc->MoveTo (rgraf.left + i * w, rgraf.top); ptrdc->LineTo (rgraf.left + i * w, rgraf.bottom); char m[10]; wsprintf (m, "%d", i * max_sls / big_ticks); int xt = i ? avg_char_width * 2 : 0; Windows MFC Programming II 492 ptrdc->TextOut (rgraf.left + i * w - xt, rgraf.bottom + 3, m); } ptrdc->MoveTo (rgraf.left, rgraf.bottom); ptrdc->LineTo (rgraf.right, rgraf.bottom); // set the initial x,y position for the first bar to be graphed to be // located just below the top of the graph vertical lines y = rgraf.top + bar_height / 2; x = rgraf.left; // gain access to the database CDaoDatabase *ptrdb = GetDocument ()->GetDB (); if (!ptrdb->IsOpen ()) return; BOOL ok = TRUE; // setup and open the Items Table to be able to retrieve the item names DAOGetItem dbitems (ptrdb); try { dbitems.Open (); } catch (CDaoException *ptrex) { AfxMessageBox (ptrex->m_pErrorInfo->m_strDescription); ptrex->Delete (); ok = FALSE; } if (!ok) return; // install primary look up key for the searches dbitems.SetCurrentIndex (_T("PrimaryKey")); ptrdc->SelectStockObject (BLACK_BRUSH); short item; int sls_w; // for each product, retrieve its name and display name and sales bar for (i=0; i<tot_items_per_cat[cat]; i++) { // convert item index into the table item number item = itemidx_to_itemnum[cat][i]; // find the item in the table dbitems.Seek (_T("="), &COleVariant (item, VT_I2)); ptrdc->SetTextAlign (TA_RIGHT); ptrdc->TextOut (rprod.right, rprod.top, dbitems.m_Item_Name); ptrdc->SetTextAlign (TA_LEFT); rprod.OffsetRect (0, bar_height * 2); // calculate the bar width based on its sales sls_w = (int) (sales[cat][i] / max_sls * rgraf.Width ()); // and draw the slaes bar ptrdc->Rectangle (x, y, x + sls_w, y + bar_height); y += bar_height * 2; } dbitems.Close (); } ... /***************************************************************************/ /* */ /* OnClearQuickView: removes the QuickView from the screen */ /* */ /***************************************************************************/ void SalesView::OnClearQuickView () { Windows MFC Programming II 493 Invalidate (); } ... In response to File|Print, the first action is to call back to the application and install the landscape option. If the calculations array exist, free all the arrays. Attempt the calculations once more, using the current, perhaps updated, data. If the calculations are successful, then invoke the base class OnFilePrint to launch the printing process. When printing is finished, free the arrays. Preview follows the same sequence, except that when control returns from the base class, do not free the arrays or the preview thread crashes. Next, the framework calls OnPreparePrinting prior to launching the print dialog box. Here the precise number of pages is know. Since I am printing one page per category, the number of categories represents the number of pages. Notice that I make no allowance for any category that has too many items in it, such that the page overflows. OnBeginPrinting and OnEndPrinting are not needed here. The framework then calls OnPrint to print each page. In order to print the page, I must know which category is to be rendered on the page. Since the category numbers begin with 0, the category for any specific page is the page number less one. When and if the page number exceeds the total number of categories, print_done is set to TRUE to halt the printing process. In the case of the CDaoRecordView, OnDraw is in use handling other matters, such as the form view. So I created RenderPage that renders any page on any passed DC. Following the framework’s practice of having the OnPrint and OnPaint functions first call OnPrepareDC followed by OnDraw, I also have OnPrint invoke our OnPrepareDC and RenderPage. However, there is one other detail to consider, the dimensions of the canvas upon which to render the page. Thus, before RenderPage is called, I construct the working rectangle based on the total dimensions of the printer. Since the DC has already been scaled in OnPrepareDC, I can then convert those device dimensions into logical ones ... OnPrepareDC (pDC, pInfo); CRect r; r.left = r.top = 0; r.right = pDC->GetDeviceCaps (HORZRES); r.bottom = pDC->GetDeviceCaps (VERTRES); pDC->DPtoLP ((CPoint*) &r, 2); RenderPage (pDC, pagenum, r); In OnPrepareDC, the now familiar CPrintInfo’s m_bContinuePrinting must be set as always. Then scaling is done. Scaling is based upon the ratio of the physical screen dimensions to the printed page dimensions. With these dimensions known, the MM_ANISOTROPIC mode is set and the window and viewport extents set if (pDC->IsPrinting ()) { CRect clrect (0, 0, GetSystemMetrics (SM_CXSCREEN), GetSystemMetrics (SM_CYSCREEN)); Windows MFC Programming II 494 CSize clsize = clrect.Size(); CSize pagesize = CSize (pDC->GetDeviceCaps (HORZRES), pDC->GetDeviceCaps (VERTRES)); pDC->SetMapMode (MM_ANISOTROPIC); pDC->SetWindowExt (clsize); pDC->SetViewportExt (pagesize); } Just for fun, the user can see a full sized sample of one report page overlaying the screen. Since the dialog controls occupy only a small portion of the screen, I can fit the report on screen quite nicely. OnQuickView follows a similar sequence that printing follows. If the calculation arrays exist, delete them; attempt to perform the calculations on the current data. If successful, then OnPrepareDC is not needed. The client rectangle is obtained from a CClientDC and the top location is moved down by 50 pixels to avoid interfering with the controls. Then RenderPage is called. Notice that you can control which page is displayed by setting which_cat to the desired category number CClientDC dc (this); CRect r; GetClientRect (&r); r.top += 50; BYTE which_cat = 0; RenderPage (&dc, which_cat + 1, r); RenderPage must draw the entire page on the passed DC. The drawing process is broken down into three steps dictated by the report design itself. The function is passed the page number to render and the rfull rectangle that defines the page dimensions. The main heading and page number and the category line are drawn by DisplayHeading which is passed a CString containing the category so that it can avoid a database access to get the category name for this page. Next the full page is divided into two portions, one for the columnar report and one for the bar chart. DisplayText and DisplayGraph then display the columnar portion and the bar chart portion respectively. The font sizes I chose are based solely on what looked good on the report. Only a few details of the rendering process must be discussed in depth. In DisplayText, two rectangles are setup one for each column, rprod and rsls for the product name and its total sales. Either TextOut or DrawText operations can be used. After displaying the column headings, and as each line is displayed, the pair of rectangles can be incremented to the next line by using the CRect member function OffsetRect passed: 0 and avg_char_height. The left and right coordinates are therefore not changed, but the top and bottom rectangles are incremented by one line. The main loop displays each item and its sales. However, the item name must be found by searching the Items Table. After obtaining a pointer to the database from the document, an instance of DAOGetItem is constructed and opened. When the Seek function is used to find the matching record in a table or query result, the current index to be used must be set. Remember that any given table could have more than one potential index schema installed. Under Access Windows MFC Programming II 495 the basic index is called PrimaryKey which is what I used. The function SetCurrentIndex is used to notify the table which index is to be used. Within the loop, which runs from index zero to tot_items_per_cat[cat], given the current index into the sales array, the corresponding item number is retrieved. The table is then searched for that record and its item name is displayed along with the sales DAOGetItem dbitems (ptrdb); ... dbitems.Open (); ... dbitems.SetCurrentIndex (_T("PrimaryKey")); ... for (i=0; i<tot_items_per_cat[cat]; i++) { item = itemidx_to_itemnum[cat][i]; dbitems.Seek (_T("="), &COleVariant (item, VT_I2)); ptrdc->TextOut (rprod.left, rprod.top, dbitems.m_Item_Name); rprod.OffsetRect (0, avg_char_height); char s[15]; sprintf (s,"%10.2lf",sales[cat][i]); ptrdc->TextOut (rsls.left, rsls.top, "$"); ptrdc->SetTextAlign (TA_RIGHT); ptrdc->TextOut (rsls.right, rsls.top, s); ptrdc->SetTextAlign (TA_LEFT); rsls.OffsetRect (0, avg_char_height); } dbitems.Close (); In DisplayGraph, I used several const int members to define the bar height, the maximum sales plotted, and the number and size of the x-axis ticks. big_ticks represents the number of vertical lines after the y-axis is drawn each a multiple of $400 and sml_ticks are those in-between markers representing $100. Once again, the Items table must be searched to find the item name to be displayed opposite its bar. I could have allocated another CString array in DisplayText and saved the looked up values and reused the array in DisplayGraph. If in fact there are substantial numbers of items, this should be done. The remainder of the graph coding is straight forward. Notice the relative ease with which external databases can be accessed and reports created.