Download Windows MFC Programming II

Document related concepts

Entity–attribute–value model wikipedia , lookup

Database 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

Extensible Storage Engine wikipedia , lookup

Database model wikipedia , lookup

Transcript
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.