Previous Page TOC Next Page



- 12 -
Printing Reports with Report Generators


Previous versions of Visual C++ were oriented toward performing actions, not printing reports. C and C++ have never been noted for built-in printing prowess, so report generation has typically fallen on the programmer's shoulders. Prior to version 4, Visual C++ didn't have any report generator facilities or classes. Visual C++ 4 includes Crystal Reports, which programmers can redistribute with their applications. Also, several companies have produced products that let C/C++ programmers add report generation facilities to their Windows programs. This chapter highlights Crystal Reports and two add-on products, ReportEase and ReportSmith (a Borland product).

The first part of this chapter looks at what it takes to use the Crystal Reports package. Crystal Reports is the product of Seagate's Crystal division. Seagate acquired Crystal Services, Inc., in May of 1994. Crystal Reports packages are now included with Visual C++ 4 and a number of versions of Visual Basic. At the time this book was written, the most current version of Crystal Reports was 4.5. Visual Basic users are still using version 3, so we're ahead of the Visual Basic users for once!

Crystal Reports is an ODBC-based report generator (like ReportSmith) that doesn't directly interact with your application's data. The application must save the data in an ODBC-accessible database. Crystal Reports then generates the report from the database's contents and the report template.

In this chapter you will generate a project that uses the Crystal Reports AppWizard add-on that lets you quickly create Visual C++ applications with complete report-writing functionality. The Crystal AppWizard add-on looks very similar to the standard Visual C++ 4 AppWizard, but it includes additional options to include Crystal Reports functionality.

The final part of this chapter looks at ReportEase and ReportSmith. There are basic differences between these two products. ReportSmith is an ODBC application (like Crystal Reports) that can be called by a Visual C++ program (using DDE techniques) or a stand-alone application, and ReportEase is a non-ODBC application that must be incorporated into your application. ReportEase doesn't have a stand-alone report generator, so you must start your application in order to use ReportEase to print reports.

Using Crystal Reports in New Visual C++ Applications


The folks at Crystal have made it easy to create a new project with Crystal Reports support built in. They've taken advantage of Visual C++ 4's ability to create custom AppWizard projects. This allows the creation of a project with Crystal Reports support built in with little or no effort on the programmer's part!

To create a new project with Crystal Reports support, start AppWizard by selecting File | New. In the New dialog box, select Project Workspace and click OK. In the New Project Workspace dialog box, select Crystal Reports AppWizard, as shown in Figure 12.1. Usually this selection will be at or near the bottom of the list, following Custom AppWizard.

Figure 12.1. The Crystal Reports selection of the New Project Workspace dialog box.

After you've selected Crystal Reports AppWizard and filled in the project's name and directory, click the Create button. The Crystal Reports AppWizard will be started, and you will see the first five Wizard dialogs that AppWizard displays for all MFC-type applications. The sixth Wizard dialog, shown in Figure 12.2, lets you configure the source of the reports that your application will allow the user to print. The two choices are to have a single, predefined report for the application or to allow the user to select reports using a dialog box.

Figure 12.2. The sixth Crystal Reports AppWizard Wizard dialog: report source.

For most applications, you'll want to let the user select from a number of predefined reports. Some simpler applications might have only one report defined. You can predefine the report to give the user easier report printing capabilities.

As soon as AppWizard has completed its design of the Crystal Reports application, you see the New Project Information dialog box, shown in Figure 12.3. This dialog lets you see what files, classes, and features are selected for your new project.

Figure 12.3. Crystal Reports' New Project Information dialog box.



NOTE

It's beyond the scope of this book to delve into the new custom AppWizard features that are part of Visual C++ 4. However, the Crystal Reports AppWizard is an excellent example of what can be done with the custom AppWizard features.


Click the OK button to have AppWizard create the project's files and then open the project in Visual C++ as the current project. You can choose to either initially build the project (a good idea) or immediately start customizing the project.

Let's take a look at what's really offered in the Crystal Reports application. Notice that the original File | Print selections are still included. Also notice that the Crystal Reports AppWizard has added a new menu called Report. Under the Report menu are the options of Open Report, Close Report, Close All, Print Preview, Printer, Export, and Mail.

As you can see in Figure 12.4, ClassWizard already has handlers for each of these menu selections.

Figure 12.4. Visual C++'s ClassWizard showing handlers for the Crystal Reports menu items.

Each of these handlers is placed in the application's CWinApp-based class. For example, in our program, the handlers are in the CCrystalReportsDemoApp class. Handlers are already written for each menu selection. (You don't have to do any programming to complete the implementation of the Crystal Reports portion of your application.)



NOTE

Although you don't have to do major programming for most implementations of Crystal Reports in a Visual C++ MFC application, you do have to design all the reports that your application requires.




NOTE

The Crystal Reports report generator program can't be redistributed. You must either design the reports for your application or arrange for your application's users to obtain licensed copies of the Crystal Reports report generator.


Each report must be created by you, the programmer. Generally, end users of your product won't have regular access to the Crystal Reports report generator, shown in Figure 12.5, but it might be possible to have end users assist in the development of the application's reports. Because report specification files aren't part of your application, you can at any point upgrade or modify the reports and redistribute them to your users. Nothing in an application is dependent on a specific report unless you design the application to open a fixed report by name.

Figure 12.5. The Crystal Reports Report Generator program.

After designing a report (or several reports), you would usually include these reports in the same directory as the main application's executable file(s). Some applications have a separate folder for report specification files, especially when the application has a large number of reports. One way to avoid the problem of end users not having access to the report generator program is to give every conceivable report a separate folder (or a hierarchy of folders). This can greatly assist the users in managing their reports.

The next part of this chapter looks at what is necessary to add Crystal Reports to an existing 32-bit MFC Windows application.



NOTE

Crystal Reports is a 32-bit application. It can be used only with Visual C++ 4, although you might be able to make it work with Visual C++ 2.x by manually writing the interface calling code. You can't use Crystal Reports with any of the 16-bit versions of Visual C++.



Adding Crystal Reports to Existing Visual C++ Applications


Sometimes an exciting new feature or option comes along after you've started developing your application. Crystal Reports and Visual C++ 4 are an example of this. Converting an existing Visual C++ 2.x application to Visual C++ 4 is a simple task. You just open the project and resave it as a Visual C++ 4 project. Adding Crystal Reports to an existing application is a bit more complex. You must manually add several new items to your application:

You also must generate any reports that the application will require.

Adding Crystal Reports to the Menu Bar


To add Crystal Reports to the application's menu bar, you must add a number of menu selections. The Crystal Reports AppWizard adds the following menu selections (in order) to the application's menu bar. Crystal Reports AppWizard adds a new top-level menu called Report, but there's no reason why you can't have this arranged as a submenu.

After you've added menu selections to the menu and assigned them identifiers (you don't have to use the identifiers that I've listed, but it will make things easier if you do), you can add the handlers for each of these menu selections using ClassWizard.

Figure 12.6 shows the Crystal Reports menu structure added as a submenu under the application's File | Print menu option.

Figure 12.6. Crystal Reports added as a submenu under File | Print.

Adding Crystal Reports Classes to Your Application


To add the classes for Crystal Reports to your application's project, first copy PEPLUS.CPP and PEPLUS.H from the Crystal Reports main directory (C:\CRW if you installed Crystal Reports in the default installation directory). The PEPLUS files contain several classes that your Crystal Reports menu handles will use.



NOTE

You need to include the Crystal Reports .LIB file, CRPE32M.LIB, in your application's project. With Visual C++ 4, including PEPLUS.H and PEPLUS.CPP in your project is sufficient to force Visual C++ to include the correct library file. Crystal Reports includes a #pragma comment(lib, "crpe32m.lib") statement in CRPE.H, which is included into your project by PEPLUS.CPP.


To insert these new files into your project, select Insert | Files Into Project, and then select the file PEPLUS.CPP. (You don't need to include PEPLUS.H explicitly. It will be picked up when dependencies are scanned.) After you've added PEPLUS.CPP, your application's ClassView tab will look something like the one shown in Figure 12.7.

Figure 12.7. Visual C++'s ClassView tab after PEPLUS.CPP has been added to the project.

Figure 12.7 shows that PEPLUS.CPP adds a large number of classes. Actually, these aren't classes; they're structures. However, the distinction between a class and a structure (a class has a constructor and a destructor, and a structure doesn't) isn't significant for the purposes of this chapter.

Adding Handlers for Crystal Reports Menu Items


Earlier you added seven new menu items to an application. These menu items now need to have both a command handler and a update handler. The command handler is used to actually perform the work of the menu item, while the update handler performs the process of enabling and disabling the menu selections as appropriate. You will examine each of the menu items and learn about the command handler and the update handler. The default update handler generally has all the menu selections except Open Report disabled unless a report is open.

All the handlers are usually placed in the CWinApp-derived class. You must also add a number of supporting variables to this class, as shown in Listing 12.1. In addition, you must add #include PEPLUS.H to the CWinApp-derived class's include file list.

Listing 12.1. Crystal Reports support variables.


CRPEngine    m_printEng;

CRPEJob     *m_pPrintJob;

char         szFileTitle[256];

Open Report...

There are two possible ways to implement the Open Report menu selection. The first and most flexible is to present an open files dialog box listing the different report files available to the user. Because Windows 95 allows long filenames, the reports can have meaningful filenames. A second implementation would be to present a (fixed) list of reports for the user to select from.



TIP

In keeping with the philosophy of not doing more work than necessary, the easiest way to create the basic code for your Crystal Reports handlers is to first create a dummy Crystal Reports application using the Crystal Reports AppWizard (or the Crystal Reports sample application included on the CD that comes with this book) and then cut and paste from this dummy application into your main application. In fact, all the handlers shown next were actually created by the Crystal Reports AppWizard when it created a sample program.


The handler for Open Report is shown in Listing 12.2. This handler shows what Crystal Reports does when it opens a report file. This code first initializes the Crystal Reports report print engine (if it's not already initialized) and then displays the Open File common dialog box to allow the user to select a filename.

If the user enters a valid filename, the Crystal Reports report engine is run with the newly opened report by calling m_pPrintJob = m_printEng.OpenJob(), which opens the Crystal Reports report job; m_pPrintJob->OutputToWindow(), which displays the Print Preview window; and m_pPrintJob->Start(), which prints the report in the Print Preview window. As Listing 12.2 shows, each of these calls has error checking to ensure that the function is successful.

Listing 12.2. The Open Report command handler.


/////////////////////////////////////////////////////////////////////////////

// CCrystalReportsDemoApp commands

// Handler for Crystal Reports Open Report Command

void CCrystalReportsDemoApp::OnOpenReport()

{

    OPENFILENAME    ofn;

    char    szFile[256];

    UINT    i;

    CString csFilter, csDirName;

    char    chReplace, *szFilter, *szDirName;

    // char szFileTitle[] defined as data member

    // Open the Crystal Reports Print Engine if it hasn't been opened.

    if ( m_printEng.GetEngineStatus() != CRPEngine::engineOpen ) {

        // Verify the Print Engine is not missing.

        VERIFY( m_printEng.GetEngineStatus() != CRPEngine::engineMissing );

        // If Print Engine cannot be opened, display the error message

        // and the Print Engine error code.

        if ( !m_printEng.Open() ) {

            AfxMessageBox( m_printEng.GetErrorText() );

        }

    }

    // THE CODE BELOW IS ONLY A SAMPLE. YOU MAY MAKE ANY CHANGE OR WRITE

    // YOUR OWN CODE FOR YOUR SPECIFIC NEEDS.

    szFile[0] = '\0';

    // Load the CRW directory

    szDirName = csDirName.GetBuffer(256);

    if ( !csDirName.LoadString(IDS_CRWDIRECTORY) ) {

        TRACE0( "Cannot load String Table" );

    }

    // Load the File_Filter String

    szFilter = csFilter.GetBuffer(256);

    if ( !csFilter.LoadString(IDS_FILTERSTRING) ) {

        TRACE0("Can not load String Table");

    }

    chReplace = csFilter[csFilter.Find('\0') - 1]; // Retrieve wildcard

    for (i = 0; szFilter[i] != '\0'; i++) {

        if (szFilter[i] == chReplace)

            szFilter[i] = '\0';

    }

    memset(&ofn, 0, sizeof(OPENFILENAME)); // Set all structure members to zero

    // Fill in struct OPENFILENAME

    ofn.lStructSize = sizeof(OPENFILENAME);

    ofn.hwndOwner = this -> m_pMainWnd->m_hWnd;

    ofn.lpstrFilter = szFilter;

    ofn.nFilterIndex = 1;

    ofn.lpstrFile = szFile;

    ofn.nMaxFile = sizeof(szFile);

    ofn.lpstrFileTitle = szFileTitle;

    ofn.nMaxFileTitle = sizeof(szFileTitle);

    ofn.lpstrInitialDir = szDirName;

    ofn.Flags = OFN_SHOWHELP | OFN_PATHMUSTEXIST |

        OFN_FILEMUSTEXIST | OFN_NOCHANGEDIR;

    // Open a Crystal Reports Print Job.

    if ( GetOpenFileName( &ofn ) ) {

        CRPEJob *pOldJob = m_pPrintJob;

        if ( ( m_pPrintJob=m_printEng.OpenJob( szFile ) ) == NULL) {

            AfxMessageBox( m_printEng.GetErrorText() );

            m_pPrintJob = pOldJob;

            return;

        }

        // YOU MAY CHANGE THE SIZE AND STYLE OF WINDOW FOR PREVIEW REPORT HERE

        if ( !m_pPrintJob->OutputToWindow(    szFileTitle, 50, 50, 550, 500,

            CW_USEDEFAULT | WS_SYSMENU | WS_THICKFRAME |

            WS_MAXIMIZEBOX | WS_MINIMIZEBOX,

            0) ) {

            AfxMessageBox( m_pPrintJob->GetErrorText() );

            m_pPrintJob = pOldJob;

            return;

        }

        if ( !m_pPrintJob->Start() ) {

            AfxMessageBox( m_pPrintJob->GetErrorText() );

            m_pPrintJob -> Close();

            m_pPrintJob = pOldJob;

        }

    }

}

It wouldn't be difficult to create multiple open reports, because the m_pPrintJob member variable returned by the call to OpenJob() could easily be stored in a list.

Since the Open Report menu option is always active, the update handler isn't used. You can code an update handler that does nothing or code no handler at all.

Close Report

Closing a report is easier than opening one. It isn't necessary to ask the user which report to close; the current report is the one that gets closed. For example, the default Crystal Reports handler for Close Report, shown in Listing 12.3, simply closes the print window, closes the actual report, and sets the print job member variable m_pPrintJob to NULL (which the update handlers use to enable and disable the other menu selections).

Listing 12.3. The Close Report command handler.


void CCrystalReportsDemoApp::OnCloseReport()

{

    // Close a Crystal Reports print job.

    m_pPrintJob -> CloseWindow();

    m_pPrintJob -> Close();

    m_pPrintJob = NULL;

}

The update handler disables the Close Report menu selection based on whether there is a currently open Crystal Reports report. The variable m_pPrintJob is non-NULL if an existing Crystal Reports report is open. Listing 12.4 shows the update handler for this menu item.

Listing 12.4. The Close Report update handler.


void CCrystalReportsDemoApp::OnUpdateCloseReport( CCmdUI* pCmdUI )

{

    pCmdUI -> Enable( m_pPrintJob != NULL );

}

Close All

Closing all reports is easier than closing a specific report. It isn't necessary to ask the user which report to close; all reports are closed automatically. For example, the default Crystal Reports handler for Close Report, shown in Listing 12.5, simply sets the print job member variable m_pPrintJob to NULL (which the update handlers use to enable and disable the other menu selections) and then calls the print engine's Close() member function.

Listing 12.5. The Close All command handler.


void CCrystalReportsDemoApp::OnCloseAllReport()

{

    // Close the Print Engine, thereby closing all print jobs.

    m_pPrintJob = NULL;

    m_printEng.Close();

}

The update handler disables the Close All menu selection based on the value returned by the GetNPrintJobs() function. Listing 12.6 shows the update handler for this menu item.

Listing 12.6. The Close All update handler.


void CCrystalReportsDemoApp::OnUpdateCloseAllReport( CCmdUI* pCmdUI )

{

    pCmdUI -> Enable( m_printEng.GetNPrintJobs() );

}

Print Preview

Crystal Reports initially calls Print Preview when a report is first opened. The user can close the Print Preview window (without closing the actual report) and then later reopen it by selecting the Crystal Reports Print Preview menu option.

As you can see from Listing 12.7, the Print Preview handler calls the OutputToWindow() function exactly as it is called in the Open Report handler. It then calls the Start() function to do the actual printing in the Print Preview window. There is less error handling in the Print Preview handler, because the Open Report handler will have closed the report if Print Preview failed at that point. If you remove the automatic Print Preview from the Open Report handler, you should make sure that your Print Report and Print Preview handlers are sufficiently rugged to handle any problems that might arise.

Listing 12.7. The Print Preview command handler.


void CCrystalReportsDemoApp::OnPreviewReport()

{

    if ( !m_pPrintJob->OutputToWindow(    szFileTitle, 50, 50, 550, 500,

         CW_USEDEFAULT | WS_SYSMENU | WS_THICKFRAME |

         WS_MAXIMIZEBOX | WS_MINIMIZEBOX, 0) ) {

        AfxMessageBox( m_pPrintJob->GetErrorText() );

        return;

    }

    if ( !m_pPrintJob->Start() )

    {

        AfxMessageBox( m_pPrintJob->GetErrorText() );

    }

}

The update handler disables the Print Preview menu selection based on whether a Crystal Reports report is currently open. The variable m_pPrintJob is non-NULL if an existing Crystal Reports report is open. Listing 12.8 shows the update handler for this menu item.

Listing 12.8. The Print Preview update handler.


void CCrystalReportsDemoApp::OnUpdatePreviewReport( CCmdUI* pCmdUI )

{

    pCmdUI -> Enable(m_pPrintJob!= NULL );

}

Printer...

Printing reports entails displaying the Windows print dialog box (which lets the user select a printer, select which pages to print, and so on) and then printing the report.

When the report is printed, the user-entered information (such as starting and ending pages) is passed to the Crystal Reports print engine. Then the Crystal Reports printout is performed by calling OutputToPrinter() and then the Start() function, as shown in Listing 12.9. Do you notice something here? The Start() function is called regardless of whether the report is going to the screen (Print Preview) or the printer.

Listing 12.9. The Print command handler.


void CCrystalReportsDemoApp::OnPrinterReport()

{

    CRPEPrintOptions printOpt;

    CPrintDialog printDlg(FALSE, PD_ALLPAGES | PD_USEDEVMODECOPIES

                                | PD_HIDEPRINTTOFILE | PD_NOSELECTION,

                          this->m_pMainWnd);

    printDlg.m_pd.nMaxPage = 0xFFFF;

    printDlg.m_pd.nMinPage = 1;

    printDlg.m_pd.nFromPage = 1;

    printDlg.m_pd.nToPage = 0xFFFF;

    if(printDlg.DoModal() == IDCANCEL)

        return;

    if( printDlg.PrintRange() ) {

        printOpt.m_startPageN = (unsigned short)printDlg.GetFromPage();

        printOpt.m_stopPageN = (unsigned short)printDlg.GetToPage();

    }

    printOpt.m_nReportCopies = (unsigned short)printDlg.GetCopies();

    m_pPrintJob -> SetPrintOptions( &printOpt );

    m_pPrintJob -> OutputToPrinter( (short)printOpt.m_nReportCopies );

    if ( !m_pPrintJob->Start() )

    {

        AfxMessageBox( m_pPrintJob->GetErrorText() );

    }

}

The update handler disables the Print menu selection based on whether there is a currently open Crystal Reports report. The variable m_pPrintJob is non-NULL if an existing Crystal Reports report is open. Listing 12.10 shows the update handler for this menu item.

Listing 12.10. The Print update handler.


void CCrystalReportsDemoApp::OnUpdatePrinterReport( CCmdUI* pCmdUI )

{

    pCmdUI -> Enable( m_pPrintJob != NULL );

}

Export...

To export a report (which writes the report's output to a file), you need to set the output option to export and then call Start() to print the report, as shown in Listing 12.11. Generally, Export works much like both Print and Print Preview, because it calls a destination (ExportTo()) and then calls Start() to do the actual output.

Listing 12.11. The Export command handler.


void CCrystalReportsDemoApp::OnExportReport()

{

    CRPEExportOptions exportOpt;

    if( m_pPrintJob->GetExportOptions( &exportOpt ) ) {

        if( !m_pPrintJob->ExportTo( &exportOpt ) ) {

            AfxMessageBox( m_pPrintJob->GetErrorText() );

            return;

        }

        if( !m_pPrintJob->Start() ) {

            AfxMessageBox( m_pPrintJob->GetErrorText() );

        }

    }

}

The update handler disables the Export menu selection based on whether there is a currently open Crystal Reports report. The variable m_pPrintJob is non-NULL if an existing Crystal Reports report is open. Listing 12.12 shows the update handler for this menu item.

Listing 12.12. The Export update handler.


void CCrystalReportsDemoApp::OnUpdateExportReport( CCmdUI* pCmdUI )

{

    pCmdUI -> Enable( m_pPrintJob != NULL );

}

Mail...

To mail a report (which writes the report's output to a file and then uses MAPI to send the report to another user), you need to set the output option to export and then call Start() to print the report, as shown in Listing 12.13. Generally, Export works much like both Print and Print Preview, because it calls a destination (ExportTo()) and then calls Start() to do the actual output.

In reality, the only difference between Export and Mail is that the user selects Mail in the Export destination rather than writing the report to a disk file. In fact, Export and Mail can be used interchangeably with Crystal Reports.



NOTE

Nothing limits you to using the same Export/Mail model that Crystal Reports uses. You could combine these two functions or write a different handler if you wanted to. For more information, look at the CRPEExportOptions structure shown in Listing 12.13.


Listing 12.13. The Mail command handler.


void CCrystalReportsDemoApp::OnMailReport()

{

    CRPEExportOptions mailOpt;

    if( m_pPrintJob->GetExportOptions( &mailOpt ) ) {

        if( !m_pPrintJob->ExportTo( &mailOpt ) ) {

            AfxMessageBox( m_pPrintJob->GetErrorText() );

            return;

        }

        if( !m_pPrintJob->Start() ) {

            AfxMessageBox( m_pPrintJob->GetErrorText() );

        }

    }

}

The update handler disables the Mail menu selection based on whether there is a currently open Crystal Reports report. The variable m_pPrintJob is non-NULL if an existing Crystal Reports report is open. Listing 12.14 shows the update handler for this menu item.

Listing 12.14. The Mail update handler.


void CCrystalReportsDemoApp::OnUpdateMailReport( CCmdUI* pCmdUI )

{

    pCmdUI -> Enable( m_pPrintJob != NULL );

}

Generating Reports for Crystal Reports


You must use the Crystal Reports report generator (CRW32.EXE) to create reports that will be printed with the Crystal Reports print engine that you are including with your application. You might find it expedient to either

Remember, the application is for the users, and if possible they should provide input into the report design phase.

Using Crystal Reports Pro


Even though Visual C++ 4 comes with Crystal Reports, the version shipped with Visual C++ isn't as full-featured as some applications might require. Crystal also has a product called Crystal Reports Pro. At the time this book was written, Crystal Reports Pro 4.5 was the only 32-bit report generator available for Visual C++ programmers.



NOTE

Unlike some producers of software, Crystal maintains and sells earlier versions of its products. This can be useful when a software project is on a restricted budget: Using earlier versions of Crystal Reports can be significantly less expensive.




NOTE

Things change rapidly in the Windows programming world. Other 32-bit report generators probably will be available by the time you read this book. If Crystal Reports Pro doesn't meet your needs, check suppliers of programming tools for other competitive products.


Table 12.1 shows the functionality that Crystal Reports Pro adds to Crystal Reports for Visual C++.

Table 12.1. Crystal Reports Pro functionality not present in Crystal Reports for Visual C++.

Functionality Description
Adding new functions Crystal Reports Pro lets you add new functions to the base product. These functions can be used to perform tasks specific to your application.
Exporting reports using Crystal Reports Pro Using Crystal Reports Pro, your applications can export reports in a number of word processor, spreadsheet, and popular data interchange formats.
Crystal custom controls bind to Visual Basic data controls New features have been added to Crystal Reports Pro that let a Crystal custom control bind directly to a Visual Basic data control. Crystal Reports Pro is bundled with TrueGrid, a bound grid control by Apex Software Corporation that lets users generate reports using the grid layout.
PEDiscardSavedData Discards data that was previously saved with the report.
PEGetGroupCondition Determines the group condition information for a selected group section in the specified report.
PEGetLineHeight Gets line height and ascent information for a specified line in a selected section of the report.
PEGetMargins Retrieves the page margin settings for the specified report.
PEGetMinimumSectionHeight Retrieves minimum section height information for selected sections in the specified report.
PEGetNDetailCopies Returns the number of copies of each Details section in the report that are to be printed.
PEGetNLinesInSection Determines the number of lines in a selected section of the specified report.
PEGetNParams Retrieves the number of parameters needed by a stored procedure.
PEGetNthParam Gets the nth parameter of a stored procedure.
PEGetPrintOptions Retrieves the print options specified for the report (the options that are set in the Print common dialog box) and uses them to fill in the PEPrintOptions structure.
PEGetReportTitle Returns the handle of the title string that is to appear on the title bar of the specified report when the report prints to a Preview window.
PEGetSectionFormat Retrieves the section format settings for selected sections in the specified report and supplies them as member values for the PESectionOptions structure.
PEGetSelectedPrinter Retrieves information about a nondefault printer if one is specified in the report.
PEGetSQLQuery Retrieves the handle for the string containing the SQL query generated by the specified report.
PEHasSavedData Queries a report to find if data is saved with it.
PEPrintControlsShowing Checks whether the print controls are displayed in the Preview window.
PESetFont Sets the font for field and/or text characters in the specified report section(s).
PESetGroupCondition Changes the group condition for a group section.
PESetLineHeight Sets the line height and ascent for a specified line in a selected section of the report.
PESetMargins Sets the page margins for the specified report to the values you supply as parameters.
PESetMinimumSectionHeight Sets the minimum height for specified report sections to the value supplied as a parameter.
PESetNDetailCopies Prints multiple copies of the Details section of the report.
PESetNthParam Sets the value of a parameter in a stored procedure.
PESetPrintOptions Sets the print options for the report to the values supplied in the PEPrintOptions structure.
PESetReportTitle Changes the Preview window title to the title string you supply as a parameter.
PESetSectionFormat Sets the section format settings for selected sections in the specified report to the values in the PESectionOptions structure.
PESetSQLQuery Changes the SQL query to the query string you supply as a parameter.
PEShowNextPage Displays the next page in the Preview window.
PEShowFirstPage Displays the first page in the Preview window.
PEShowPreviousPage Displays the previous page in the Preview window.
PEShowLastPage Displays the last page in the Preview window.
PEShowPrintControls Displays the print controls.

You can include Crystal Reports Pro in a Visual C++ project using the same techniques that you would use to include Crystal Reports for Visual C++. Many applications will do just fine using Crystal Reports for Visual C++, but mainstream applications will benefit from Crystal Reports Pro's increased functionality.



NOTE

Crystal maintains a flexible licensing policy for developers who need to include Crystal Report's report generator module with their applications. Contact Crystal at (604) 681-3435 and talk to one of their OEM account managers. See Appendix A, "Resources for Developing Visual C++ Database Applications," for more information about Crystal.



Using ReportEase with Visual C++ Applications


The ReportEase program is installed as a DLL that can be called from your own applications. You also have the option of simply including the ReportEase source code with your application if you don't want to ship separate DLL files. One of the best features of ReportEase is that it's supplied with full source code, which lets developers make modifications to enhance the program's usability with the calling application. Also, you can fix those little annoying bugs that are often found in third-party applications.

As this book was being written, Sub Systems announced version 2.5 of ReportEase. This new version sports a number of new functionalities:

Due to the fact that the new version of ReportEase wasn't available for review, this chapter discusses the earlier version.

This section of the chapter looks at the interface that ReportEase uses. You will also see examples of the code that is necessary to call ReportEase from a Windows application. The calling program in this instance is STARmanager, a sales territory GIS management system. STARmanager is a commercial Windows application available from

Woburn, MA 01801

ReportEase is designed to accept records from one or more logical files or sources. These records don't need to be kept in separate disk files, because when the report printer is run, your interface will supply the necessary data to the ReportEase report printer module.

Using ReportEase is a two-step process. First you must create a report (using the form editor), and then you can print it. You use the report printer to print either to the screen or to a printer. You must supply some common support routines. It's easy to actually incorporate ReportEase—the initial inclusion usually takes only a few hours.



NOTE

Because including ReportEase with a Visual C++ 4 project requires programming that is specific to each application, the source files in this part of the chapter are for example only. Your own implementation may vary significantly.



Creating Reports with ReportEase


When you use ReportEase, the first interface you need to create is the form editor interface. Listing 12.15 shows an example of the code that calls the form generator from a C++ application. All the examples in the ReportEase documentation are for C programs; however, ReportEase can be used with both C and C++ programs.

Listing 12.15. Calling the ReportEase form generator.


/*  REPORTS.C */

#define  HAVEREPORT

#include "stdafx.h"

#include "STAR.H"

#include <time.h>

#include <stdio.h>

#include <stdlib.h>

#include <string.h>

#include <io.h>

#define REP_CPLUSPLUS extern "C"

#include "rep\rep.h"

#include "reportco.h"

/******************************************************************************

**

**      TITLE: REPSYS.cpp

**

**   FUNCTION: Calls ReportEase PLUS's report generator to create

**             a new report or edit an existing report.

**

******************************************************************************/

// LOCAL PROTOTYPES:

void InitDataField(void);

int FAR PASCAL _export UserFieldSelection(HWND hWnd,

    struct  StrField huge * field, int SortFieldNo);

int FAR PASCAL _export VerifyField(struct StrField huge * field,

    int SortFieldNo);

// END LOCALS

// Externs

extern  HWND    hDlgPrint;

extern  BOOL    bUserAbort;

extern  int  nCurrentSelection;

// Extern  REPORT   Report;

extern  WORD    wReportType;

// End externs

// Defines

#define MAX_FILES    1  /* One file, the database...    */

#define MAX_FIELDS  65  /* Possible fields              */

int     nReportNumberFields = {0};

// End defines

#pragma pack(1)

struct StrDataField

{

        char    name[35];       // Field name

        int     width;          // Field width

        int     type;           // Field type

        int     DecPlaces;      // Decimal places

        int     FieldId;

} DataField[MAX_FILES][MAX_FIELDS];

#pragma pack()

#pragma pack(1)

StrForm FormParam;

#pragma pack()

void    ReportSystem(HWND       hWndParent)

{

//      First, initialize the DataField structure.

//      Initialize the report's columns

        InitDataField();

//      Got report filename. Now set up for the form generator.

        FormParam.x = CW_USEDEFAULT;

        FormParam.y = CW_USEDEFAULT;

        FormParam.width = CW_USEDEFAULT;

        FormParam.height = CW_USEDEFAULT;

        FormParam.UserSelection = UserFieldSelection;

        FormParam.VerifyField = VerifyField;

        strcpy(FormParam.file, ReportStuff.szFileName);

        strcpy(FormParam.DataSetName, "HUH");

        FormParam.ShowMenu = TRUE;

        FormParam.ShowHorBar = TRUE;

        FormParam.ShowVerBar = TRUE;

        FormParam.hInst = hInst;

        FormParam.hPrevInst = NULL;

        FormParam.hParentWnd = hWndParent;

        FormParam.hFrWnd = 0;

        FormParam.style=WS_OVERLAPPEDWINDOW;  // Editor window style

        FormParam.FontTypeFace[0] = '\0';

        FormParam.EndForm = NULL;

        FormParam.open = FALSE;

        FormParam.modified = FALSE;

//      It's set up. Now call the form generator:

        form(&FormParam);

//      Set up stuff to return, like the report's filename:

        strcpy(ReportStuff.szFileName, FormParam.file);

        return;

}

Figure 12.8 shows the ReportEase report generator being called using the code shown next.

Figure 12.8. ReportEase's report generator in action.

To incorporate ReportEase into your application, you must do the following. First, you must include an #include directive for the ReportEase include file, REP.H. This file contains the definitions that are needed to create the structures used by ReportEase. Next you must create the data structures that are used to hold information about the fields that will be available to the user when the report is being designed. The StrDataField structure is used for this purpose. It's configured as shown in this code fragment:


#define MAX_FILES    1   /* One file, the database... */

#define MAX_FIELDS  65   /* 65 possible fields */

struct StrDataField

{

    char       name[35];     // Field name

    int        width;        // Field width

    int        type;         // Field type

    int        DecPlaces;    // Decimal places

    int        FieldId;      // Identifier for each field

} DataField[MAX_FILES][MAX_FIELDS];

The DataField structure must contain certain information, but you can also include any additional information that might help your application process columns in the report. In my program, a field called FieldId has been added to hold an identifier for each field (otherwise, you would have to do string compares on the name field) to let a field be easily identified when the report is being processed.

The fields generally are self-explanatory, with the exception of the type field, which will contain values defined in REP.H. The valid values for the type field appear in Table 12.2.

Table 12.2. Values for the type field.

Value Description
TYPE_TEXT A text field. Text operations are allowed on this field.
TYPE_NUM A fixed-point numeric field. Numeric operations are allowed on this field. The number of digits after the decimal point should be stored in the DecPlaces field. The user can alter the number of decimal places when the report form is generated.
TYPE_DBL A double-precision floating-point numeric field. Numeric operations are allowed on this field. The number of digits after the decimal point should be stored in the DecPlaces field. The user can alter the number of decimal places when the report form is generated.
TYPE_DATE A date field. Dates are stored in a long integer (32-bit) field in the format of either YYMMDD or YYYYMMDD.
TYPE_LOGICAL A numeric field. Logical data is stored as either a long integer (32-bit) 1 or 0.

Next you must write a function to let the user select fields to place in the report. Here is the function prototype for this function:


int far PASCAL UserFieldSelection(HWND hWndParent,

               struct StrField,

    far * field, int SortFieldNumber);

This function can display a simple dialog box that contains a list box and an OK button. Figure 12.9 shows a typical field selection dialog box.

Figure 12.9. A field selection dialog box.

Listing 12.16 shows the code necessary to manage the field selection dialog box shown in Figure 12.9. Much of this code was actually generated using ClassWizard.

Listing 12.16. REPORTCO.CPP: Field selection dialog box code.


// reportco.cpp : implementation file

//

#include "stdafx.h"

#include "starae.h"

#include "reportco.h"

#ifdef _DEBUG

#undef THIS_FILE

static char BASED_CODE THIS_FILE[] = __FILE__;

#endif

/////////////////////////////////////////////////////////////////////////////

// CReportColumns dialog

CReportColumns::CReportColumns(CWnd* pParent /*=NULL*/)

    : CDialog(CReportColumns::IDD, pParent)

{

    //{{AFX_DATA_INIT(CReportColumns)

    m_ColumnName = -1;

    //}}AFX_DATA_INIT

}

void CReportColumns::DoDataExchange(CDataExchange* pDX)

{

    CDialog::DoDataExchange(pDX);

    //{{AFX_DATA_MAP(CReportColumns)

    DDX_Control(pDX, IDC_REPORT_COLUMN, m_ColumnControl);

    DDX_CBIndex(pDX, IDC_REPORT_COLUMN, m_ColumnName);

    //}}AFX_DATA_MAP

}

BEGIN_MESSAGE_MAP(CReportColumns, CDialog)

    //{{AFX_MSG_MAP(CReportColumns)

    //}}AFX_MSG_MAP

END_MESSAGE_MAP()

/////////////////////////////////////////////////////////////////////////////

// CReportColumns message handlers

// These defines are *also* in RepSys.CPP

//

#define MAX_FILES    1    /* One file, the database...    */

#define MAX_FIELDS  65    /* 65 possible fields           */

extern    int    nReportNumberFields;

// End defines

#pragma pack(1)

extern struct StrDataField

{

    char    name[35];       // Field name

    int        width;       // Field width

    int        type;        // Field type

    int        DecPlaces;   // Decimal places

    int        FieldId;     // Unique identifier for field

} DataField[MAX_FILES][MAX_FIELDS];

#pragma pack()

BOOL CReportColumns::OnInitDialog()

{

    CDialog::OnInitDialog();

    // TODO: Add extra initialization here

int        i = 0;

    while (DataField[0][i].name[0] && i <=  nReportNumberFields)

    {

        m_ColumnControl.AddString(DataField[0][i].name);

        ++i;

    }

    m_ColumnControl.SetCurSel(0);

    return TRUE;  // Return TRUE  unless you set the focus to a control

}

The code to create the dialog box is shown next. This code works with a minimum amount of programming because the m_ColumnName is bound to the dialog box combo box control and is automatically updated each time the user makes a selection from the combo box. If m_ColumnName is less than zero, the user didn't have a column selected.

Here's a typical UserFieldSelection() function:


int FAR PASCAL _export UserFieldSelection(

HWND hWnd,

struct    StrField huge * field,

int        SortFieldNo)

{

// Display dialog box of field names and allow user to select one

// Return (TRUE) if selection was successful or !TRUE if unsuccessful

// Allow the user to select from a dialog box...

int        CurFile = 0;

int        CurField = -1;

int        nReturn = IDCANCEL;

    // CWnd used to tell dialog to return to report generator, not star

    CWnd cwForm;

    cwForm.Attach(FormParam.hFrWnd);

    CReportColumns reportcolumn(&cwForm);

    reportcolumn.m_ColumnName = 0;

    if (reportcolumn.DoModal() == IDOK &&

        reportcolumn.m_ColumnName >= 0)

    {

        CurField = reportcolumn.m_ColumnName;

        nReturn = IDOK;

    }

    cwForm.Detach();  // So we don't blow up in destructor

    if (nReturn != IDCANCEL)

    {//    User selected a column to use.

     //    Now save it...

        lstrcpy(field->name, DataField[CurFile][CurField].name);  // Field name

        field->type = DataField[CurFile][CurField].type;   // Alpha/num, etc.

        field->width = DataField[CurFile][CurField].width;  // Display width

        field->DecPlaces = DataField[CurFile][CurField].DecPlaces;  // Dec place

        field->FieldId = CurField;                      // Column ID (optional)

        field->FileId = 0;                              // File ID (optional)

        return (TRUE);

    }

    return (FALSE);

}


TIP

Notice how a CWnd object is used to set which window (the ReportEase window) will have focus during the field selection process. This was done to prevent focus from returning to the main application window.


Notice how each of the fields in the StrField structure is filled out based on the user's selection, and then TRUE is returned. If the user doesn't select a field (usually by clicking the Cancel button), FALSE is returned. Here are the fields that are required to be filled in:

Two optional, but useful, fields may be filled in as well. They let the field be more easily identified:

Each of these fields is filled out using information stored in your DataField structure, so if you're filling out field->FileId or field->FieldId, you must add fields to your DataField structure for field->FileId or field->FieldId.

In addition to the field selection function, you must also provide a field verification function. This function is called by the form editor whenever a field must be verified as valid. This is necessary because a user who is creating a report might also directly enter a field name without using the field selection function that was described earlier.

The field verification function has the following prototype:


int far PASCAL VerifyField(struct StrField far * field,

    int SortFieldNo);

This function is called with the first parameter pointing to a field structure. The second parameter tells if the field is being sorted on. If SortFieldNo is zero, the field is not a sort field. If SortFieldNo is nonzero, this field is being sorted. This function should return TRUE if the field is valid and FALSE if the field isn't valid. Typically, the VerifyField() function will take the field name and attempt to look it up in a list of fields that are valid for this report.

The example shown next loops through the DataField structure's members. When a match is found, the field structure is reinitialized (which ReportEase requires) and TRUE is returned. If no match is found, the field isn't valid and the function returns FALSE. Because the program allows sorting on any field, the SortFieldNo parameter can be ignored. If the program doesn't allow sorting of certain fields, the VerifyField() function should return FALSE for those fields.


int FAR PASCAL export VerifyField(

struct    StrField huge * field,

int        SortFieldNo)

{

int        i;  // Loop counter

int        nReturnCode = FALSE;  // Not found yet

// Verify the field as correct

// Return (TRUE) if field is valid and !TRUE if invalid

//  First, if the DataField[] structure is not initialized,

//  initialize it (happens when an existing report is

//  reopened in subsequent runs):

    if (nReportNumberFields == 0)

        InitDataField();

    for (i = 0; i < nReportNumberFields && !nReturnCode; i++)

    {

           if (_stricmp(DataField[0][i].name, field[0].name) == 0)

           {// Found a match!

                field[SortFieldNo].FieldId   = DataField[0][i].FieldId;

                field[SortFieldNo].FileId    = 0;

                field[SortFieldNo].width     = DataField[0][i].width;

                field[SortFieldNo].type      = DataField[0][i].type;

                field[SortFieldNo].DecPlaces = DataField[0][i].DecPlaces;

                nReturnCode                  = TRUE;  // Found it. Go home.

           }

    }

    return(nReturnCode);

}

The call to InitDataField() is made to initialize the DataField structure. This function is shown in Listing 12.17. The InitDataField() function shown sets up a printed report for a GIS-type application. This report example has a substantial number of fields.

Listing 12.17. INITDF.C: The InitDataField() function.


void InitDataField()

{// Init data fields. Should read from resources in actual code.

int        i;

nReportNumberFields = 0;

    strcpy(DataField[0][nReportNumberFields].name, "Basemap");

    DataField[0][nReportNumberFields].width = 15;

    DataField[0][nReportNumberFields].type = TYPE_TEXT;

    DataField[0][nReportNumberFields].DecPlaces = 0;

    for (i = 1; i < 8; i++)

    {//    Dup the name/description parameters...

        DataField[0][i] = DataField[0][i - 1];

    }

    for (i = 0; i < MAX_FIELDS; i++)

    {// Init the field IDs:

        DataField[0][i].FieldId        = i;

    }

    strcpy(DataField[0][++nReportNumberFields].name,  "BasemapName");

    strcpy(DataField[0][++nReportNumberFields].name,  "Territory");

    strcpy(DataField[0][++nReportNumberFields].name,  "TerritoryName");

    strcpy(DataField[0][++nReportNumberFields].name,  "District");

    strcpy(DataField[0][++nReportNumberFields].name,  "DistrictName");

    strcpy(DataField[0][++nReportNumberFields].name,  "Region");

    strcpy(DataField[0][++nReportNumberFields].name,  "RegionName");

    strcpy(DataField[0][++nReportNumberFields].name,  "State");

    DataField[0][nReportNumberFields].width = 2;

    DataField[0][nReportNumberFields].type = TYPE_TEXT;

    DataField[0][nReportNumberFields].DecPlaces = 0;

    strcpy(DataField[0][++nReportNumberFields].name,  "Account");

    DataField[0][nReportNumberFields].width = 3;

    DataField[0][nReportNumberFields].type = TYPE_LOGICAL;

    DataField[0][nReportNumberFields].DecPlaces = 0;

    strcpy(DataField[0][++nReportNumberFields].name,  "AccountLocation");

    DataField[0][nReportNumberFields].width = 15;

    DataField[0][nReportNumberFields].type = TYPE_TEXT;

    DataField[0][nReportNumberFields].DecPlaces = 0;

    strcpy(DataField[0][++nReportNumberFields].name,  "AccountType");

    DataField[0][nReportNumberFields].width = 15;

    DataField[0][nReportNumberFields].type = TYPE_TEXT;

    DataField[0][nReportNumberFields].DecPlaces = 0;

    strcpy(DataField[0][++nReportNumberFields].name,  "InView");

    DataField[0][nReportNumberFields].width = 3;

    DataField[0][nReportNumberFields].type = TYPE_LOGICAL;

    DataField[0][nReportNumberFields].DecPlaces = 0;

    for (i = 0; i <20; ++i)

    {//    Create DataVariable 1 - 20:

        if (DataVariable[i].bValidData && !DataVariable[i].bIsAFormula)

        {

            ++nReportNumberFields;

            DataField[0][nReportNumberFields].FieldId = i + 100;

            // 100 is flag for a data field

            DataField[0][nReportNumberFields].width = 10;

            DataField[0][nReportNumberFields].type = TYPE_DBL;

            DataField[0][nReportNumberFields].DecPlaces = 2;

            sprintf(DataField[0][nReportNumberFields].name,

                DataVariable[i].szDescription);

        }

    }

    for (i = 0; i <20; ++i)

    {//    Create DataVariable 1 - 20:

        ++nReportNumberFields;  // Increment to first data field... then loop

        DataField[0][nReportNumberFields].FieldId = i + 100;

        // 100 is flag for a data field

        DataField[0][nReportNumberFields].width = 10;

        DataField[0][nReportNumberFields].type = TYPE_DBL;

        DataField[0][nReportNumberFields].DecPlaces = 2;

        sprintf(DataField[0][nReportNumberFields].name,

            "DataVariable%d", i + 1);

    }

    return;

}

After you've set up your DataField structure and written your VerifyField() and UserFieldSelection() functions, you must then set up the StrForm structure that is passed to the form editor. Listing 12.18 shows the StrForm structure.

Listing 12.18. STRFORM.H.


typedef struct _StrForm {

       int    x;       // Initial x position of the editing window.

                       // You may specify CW_USEDEFAULT to use default values.

       int    y;       // Initial y position of the editing window.

       int    width;   // Initial width of the window in device units.

                       // You may specify CW_USEDEFAULT to use default values.

       int    height;  // Initial height of the editing window. When you

                       // use CW_USEDEFAULT for width, the height parameter is

                       // ingnored.*/

       int (FAR PASCAL _export *UserSelection)

                                (HWND, struct StrField huge *,int);

                           /* A pointer to the function returning the user-

                           selected field through the structure pointer.

                           Your application returns the chosen field through

                           the first parameter. The second argument indicates

                           the sort field number. If it is equal to zero,

                           it means that the field being sought is not a sort

                           field. The function returns a TRUE value if

                           successful.

                           When using ReportEase as a DLL, this pointer must

                           be passed using the MakeProcInstance function. Your

                           application must define this function as

                           exportable and include it in the EXPORT section

                           of the definition file.

                           */

       int (FAR PASCAL _export *VerifyField)(struct StrField huge *,int);

                           /* A pointer to the user routine that validates a

                           field name. The field name is passed to the routine

                           by the 'name' variable in the StrField structure.

                           This routine should also fill the 'type' variable

                           in the structure. The second argument indicates the

                           sort field number. If it is equal to zero, it means

                           that the field to verify is not a sort field. The

                           function returns TRUE to indicate a valid field.

                           When using ReportEase as a DLL, this pointer must

                           be passed using the MakeProcInstance function. Your

                           application must define this function as

                           exportable and include it in the EXPORT section

                           of the definition file.

                           */

       char file[130];  // Form filename. If an existing file is specified,

                        // the following fields are ignored.

       char DataSetName[20];/* (Specify for a new file) Your application can

                           specify a data set name that can be used to

                           associate the application data to the form. This

                           is an optional field. */

        BOOL   ShowMenu;   // Show the menu bar?

        BOOL   ShowHorBar; // Show the horizonatal scroll bar

        BOOL   ShowVerBar; // Show the vertical scroll bar

        HANDLE hInst;      // Handle of the current instances.

        HANDLE hPrevInst;  // Handle of the previous instances.

        HWND   hParentWnd; // Handle to the parent window

        HWND   hFrWnd;     // Form main window handle; will be filled by

                           // RE later

        DWORD  style;      //  Editor window style

        char   FontTypeFace[32]; // Default type face, example:

                           // TmwRmn Courier, etc.

        LPCATCHBUF EndForm;  // Error return location

        BOOL       open;     // TRUE indicates an open window

                             // (parameter block in use)

        BOOL       modified; // TRUE when the file modified

                             // and needs to be saved

       }StrForm;

The StrForm structure is used to pass information to the ReportEase report editor. Contained in this structure is information such as the window defaults, the routines that are used to select and verify report variables, fonts, and other information. An example of using the StrForm structure is shown in Listing 12.19.

Listing 12.19. Using StrForm.


void    ReportSystem(HWND    hWndParent)

{// Now using STARREPT (derived from ReportEase Plus)

//    First, initialize the DataField structure

//    Initialize the report's columns

    InitDataField();

//    Got report filename. Now set up for the form generator.

    FormParam.x = CW_USEDEFAULT;

    FormParam.y = CW_USEDEFAULT;

    FormParam.width = CW_USEDEFAULT;

    FormParam.height = CW_USEDEFAULT;

    FormParam.UserSelection = UserFieldSelection;

    FormParam.VerifyField = VerifyField;

    strcpy(FormParam.file, ReportStuff.szFileName);

    strcpy(FormParam.DataSetName, "HUH");

    FormParam.ShowMenu = TRUE;

    FormParam.ShowHorBar = TRUE;

    FormParam.ShowVerBar = TRUE;

    FormParam.hInst = hInst;

    FormParam.hPrevInst = NULL;

    FormParam.hParentWnd = hWndParent;

    FormParam.hFrWnd = 0;

    FormParam.style=WS_OVERLAPPEDWINDOW;  // Editor window style

    FormParam.FontTypeFace[0] = '\0';

    FormParam.EndForm = NULL;

    FormParam.open = FALSE;

    FormParam.modified = FALSE;

Finally, the following code fragment (the final part of the ReportSystem() function) calls the form editor that the user can use to create or modify the report:


// It's set up. Now call the form generator:

    form(&FormParam);

    return;

}

The call to form() returns immediately, and the report editor module starts. The report editor has focus, but the user may set focus to the calling application if desired.

Printing Reports with ReportEase


After the user has created a report with ReportEase, the next step is to print the report. When ReportEase exits, it prompts the user to save the generated report so that the calling application doesn't need to worry about the details of saving the report definition.

Listing 12.20 shows the code used to initialize the report printer, sort the data, pass the records to the report printer, and clean up after printing the report. Reports may be printed either to the screen (in a print preview mode) or to an actual printer. When the report is printed to the screen, the report printer maintains a buffer of all the records in the report to allow the user to scroll forward and backward in the report.

Figure 12.10 shows the ReportEase report previewer with a sample report.

Figure 12.10. ReportEase's report generator in screen preview mode.



NOTE

When calling C code from a C++ application, you must "wrap" the C function prototypes in a C block, like this:




This tells the compiler and linker that the functions are C code and not C++.

Listing 12.20. REPPRINT.CPP: The report printer for ReportEase.

/*  REPPRINT.CPP */

#define  HAVEREPORT

#include "stdafx.h"

#include "STAR.H"

#include <time.h>

#include <stdio.h>

#include <stdlib.h>

#include <string.h>

#include <io.h>

#define REP_CPLUSPLUS extern "C"

#include "rep\rep.h"

#include "reportco.h"

/******************************************************************************

**

**       STAR: The Sales Territory Alignment Resource.

**

**      TITLE: REPPRINT.cpp

**

**   FUNCTION: Calls to ReportEase PLUS's report printer, to actually

**                print (to screen or printer) a report.

**

**     INPUTS: From the user

**

**    OUTPUTS: Report definition file

**

**    RETURNS: YES

**

**    WRITTEN: July 26, 1994

**

**      CALLS: ReportEase routines in STARREPT.DLL

**

**  CALLED BY: Star, from report, new/edit menu selection

**

**     AUTHOR: Peter D. Hipson

**

**      NOTES: For Windows 3.1+

**

**   COPYRIGHT 1990 - 1994 BY PETER D. HIPSON. All rights reserved.

**

******************************************************************************/

// LOCAL PROTOTYPES:

// END LOCALS

// Externs

extern  HWND    hDlgPrint;

extern  BOOL    bUserAbort;

extern  int  nCurrentSelection;

extern  WORD    wReportType;

int FAR PASCAL _export VerifyField(

    struct StrField huge * field, int SortFieldNo);

// End externs

// Defines

#define MAX_FILES   1   /* One file, the database...    */

#define MAX_FIELDS  30  /* 30 possible fields           */

// End defines

#pragma pack(1)

#ifdef __cplusplus

extern "C" { // De-mangle so that .C code can reference this variable:

#endif

StrRep  Report;

#ifdef __cplusplus

}

#endif

extern StrForm  FormParam;  // Defined in RepSys.CPP

#pragma pack()

void    ReportPrinter()

{// from ReportEase Plus

struct  StrField far *fld;

BOOL    bHaveRegions;

BOOL    bHaveDistricts;

BOOL    bHaveTerritories;

BOOL    bHaveGeocodes;

BOOL    bUseThisRecord;

DWORD   dwOldRegion;

DWORD   dwOldDistrict;

DWORD   dwOldTerritory;

DWORD   dwNextRecord;

int     i;

int     nErrorOccured = FALSE;

REPORTSORT  ReportSort;

char        szSortFileName[256];

CString     CState;

    HourGlass(TRUE);

    memset(&Report, '\0', sizeof(Report));

//  Save our filename and set a few other parameters:

    strcpy(Report.file, ReportStuff.szFileName);

    if (ReportStuff.nScreen == 0)  // Set the report destination

        Report.device='S';

    else

    {

        Report.device='P';

        HourGlass(TRUE);

    }

    if (FormParam.width == 0 && FormParam.height == 0)

    {// Specify the window coordinates for screen. New run, no default!

        RECT    rect;

        ::GetClientRect(hWndView, &rect);

        ::ClientToScreen(hWndView, (POINT *)&rect.left);

        ::ClientToScreen(hWndView, (POINT *)&rect.right);

        Report.x = rect.left;

        Report.y = rect.top;

        Report.width = rect.right - rect.left;

        Report.height = rect.bottom - rect.top;

    }

    else

    {// Report generator ran already; have defaults.

        Report.x=FormParam.x;

        Report.y=FormParam.y;

        Report.width=FormParam.width;    // Screen only (3/4)

        Report.height=FormParam.height;  // Screen only (4/4)

    }

    Report.hInst = hInst;

    Report.hParentWnd = hWnd;

    if (RepInit(&Report)!=0)  // Initialize ReportEase system...

    {// This is what happens if it fails:

        TRACE("RepInit()... failed!\n");

        return;

    }

//  For each record in VIEW, process:

    (void)GetTempFileName(0, "STR", 0, szSortFileName);

    CFile sortfile;

    TRY

    {

        sortfile.Open(szSortFileName,

            CFile::modeCreate | CFile::modeReadWrite);

    }

    CATCH( CFileException, e )

    {

        #ifdef _DEBUG

            afxDump << "File could not be opened " << e->m_cause << "\n";

        #endif

    }

    END_CATCH

// Figure out if it's a Region, District, Territory, or Geocode level report!

    bHaveRegions = FALSE;

    bHaveDistricts = FALSE;

    bHaveTerritories = FALSE;

    bHaveGeocodes = FALSE;

    fld = Report.field;

    for (i = 0; i < Report.TotalFields; i++)

    {

        if (fld[i].source == SRC_APPL)

        {

            switch(fld[i].FieldId)

            {// Need #defines here!

                case 0: // Basemap

                    bHaveRegions = TRUE;

                    bHaveDistricts = TRUE;

                    bHaveTerritories = TRUE;

                    bHaveGeocodes = TRUE;

                    break;

                case 1: // Basemap name

                    bHaveRegions = TRUE;

                    bHaveDistricts = TRUE;

                    bHaveTerritories = TRUE;

                    bHaveGeocodes = TRUE;

                    break;

                case 2: // Territory

                    bHaveRegions = TRUE;

                    bHaveDistricts = TRUE;

                    bHaveTerritories = TRUE;

                    break;

                case 3: // Territory name

                    bHaveRegions = TRUE;

                    bHaveDistricts = TRUE;

                    bHaveTerritories = TRUE;

                    break;

                case 4: // District

                    bHaveRegions = TRUE;

                    bHaveDistricts = TRUE;

                    break;

                case 5: // District name

                    bHaveRegions = TRUE;

                    bHaveDistricts = TRUE;

                    break;

                case 6: // Region

                    bHaveRegions = TRUE;

                    break;

                case 7: // Region name

                    bHaveRegions = TRUE;

                    break;

                default:

                    break;

            }

        }

    }

    if (!bHaveRegions && !bHaveDistricts &&

        !bHaveTerritories && !bHaveGeocodes)

    {// If none, give them all (a strange report, but then...)

        bHaveRegions = TRUE;

        bHaveDistricts = TRUE;

        bHaveTerritories = TRUE;

        bHaveGeocodes = TRUE;

    }

//===================

    dwOldRegion = 0;

    dwOldDistrict = 0;

    dwOldTerritory = 0;

    if (ReportStuff.bReportOnView)

    {// Force to in view for testing!

        dwNextRecord = GetFirstInProblem(&Info.ipSource);

    }

    else

    {

        dwNextRecord = GetFirstInDatabase(&Info.ipSource);

    }

    while(dwNextRecord)

    {// Now process this record:

        bUseThisRecord = TRUE;  // Always use this record, unless:

        if (bHaveTerritories && dwOldTerritory !=

            Info.ipSource.gcTerritory.dwSelf)

            bUseThisRecord = TRUE;

        if (bHaveTerritories && dwOldTerritory !=

            Info.ipSource.gcTerritory.dwSelf)

            bUseThisRecord = TRUE;

        if (bHaveDistricts && dwOldDistrict !=

            Info.ipSource.gcDistrict.dwSelf)

            bUseThisRecord = TRUE;

        if (bHaveRegions && dwOldRegion !=  Info.ipSource.gcRegion.dwSelf)

            bUseThisRecord = TRUE;

        if (Info.ipSource.gcGeocode.dwSelf == 0)  // Never use <none> records

            bUseThisRecord = FALSE;

        if (ReportStuff.bExcludeUnassigned &&

            Info.ipSource.gcTerritory.Polygon.bStateRecord)

        {

            bUseThisRecord = FALSE;

        }

        dwOldRegion = Info.ipSource.gcRegion.dwSelf;

        dwOldDistrict = Info.ipSource.gcDistrict.dwSelf;

        dwOldTerritory = Info.ipSource.gcTerritory.dwSelf;

        if (bUseThisRecord)

        {

            memset(&ReportSort, '\0', sizeof(ReportSort));

            ReportSort.gcGeocode = Info.ipSource.gcGeocode;

            strcpy(ReportSort.szTerritory,

                Info.ipSource.gcTerritory.Polygon.szGeocode);

            strcpy(ReportSort.szTerritoryName,

                Info.ipSource.gcTerritory.Polygon.szGeocodeName);

            strcpy(ReportSort.szDistrict,

                Info.ipSource.gcDistrict.Polygon.szGeocode);

            strcpy(ReportSort.szDistrictName,

                Info.ipSource.gcDistrict.Polygon.szGeocodeName);

            strcpy(ReportSort.szRegion,

                Info.ipSource.gcRegion.Polygon.szGeocode);

            strcpy(ReportSort.szRegionName,

                Info.ipSource.gcRegion.Polygon.szGeocodeName);

            Info.ipSource.gcGeocode.Polygon.bInCurrentProblem =

                 Info.ipSource.gcTerritory.Polygon.bInCurrentProblem;

            if (Info.ipSource.gcGeocode.bAccount)

            {

                GetWorkFileRecord(Info.ipSource.gcGeocode.dwLocatedIn,

                    &Info.ipSource.gcAccount);

                strcpy(ReportSort.szAccountContainer,

                    Info.ipSource.gcAccount.Polygon.szGeocode);

            }

            TRY

            {

                sortfile.Write(&ReportSort, sizeof(ReportSort));

            }

            CATCH( CFileException, e )

            {

                #ifdef _DEBUG

                    afxDump << "Cannot write to file " << e->m_cause << "\n";

                #endif

            }

            END_CATCH

        }

        // Get next record (if any) in database/view:

        if (ReportStuff.bReportOnView)

        {

            dwNextRecord = GetNextInProblem(&Info.ipSource, FALSE);

        }

        else

        {

            dwNextRecord = GetNextInDatabase(&Info.ipSource);

        }

    }

//  Sort the workfile. Will have to close it, then reopen after sort:

//  Do actual sort, calling a file sort function:

    if (Report.TotalSortFields > 0)

    {// Then report must be sorted:

        sortfile.Close();

        SortReport(szSortFileName);

        TRY

        {

            sortfile.Open(szSortFileName, CFile::modeReadWrite);

        }

        CATCH( CFileException, e )

        {

            #ifdef _DEBUG

                afxDump << "File could not be opened " << e->m_cause << "\n";

            #endif

        }

        END_CATCH

    }

//  Done with sort. Now read each record in workfile, then do 'em:

    sortfile.SeekToBegin();

    fld = Report.field;

    HourGlass(FALSE);

    while (!nErrorOccured &&

         sortfile.Read(&ReportSort, sizeof(ReportSort)) == sizeof(ReportSort))

    {

        for (i = 0; i < Report.TotalFields; i++)

        {

            if (fld[i].source == SRC_APPL)

            {

                switch(fld[i].FieldId)

                {// Need #defines here!

                    case 0: // Basemap

                        strcpy(fld[i].CharData,

                            ReportSort.gcGeocode.Polygon.szGeocode);

                        break;

                    case 1: // Basemap name

                        strcpy(fld[i].CharData,

                            ReportSort.gcGeocode.Polygon.szGeocodeName);

                        break;

                    case 2: // Territory

                        strcpy(fld[i].CharData, ReportSort.szTerritory);

                        break;

                    case 3: // Territory name

                        strcpy(fld[i].CharData, ReportSort.szTerritoryName);

                        break;

                    case 4: // District

                        strcpy(fld[i].CharData, ReportSort.szDistrict);

                        break;

                    case 5: // District name

                        strcpy(fld[i].CharData, ReportSort.szDistrictName);

                        break;

                    case 6: // Region

                        strcpy(fld[i].CharData, ReportSort.szRegion);

                        break;

                    case 7: // Region name

                        strcpy(fld[i].CharData, ReportSort.szRegionName);

                        break;

                    case 8: // State

                        CState.LoadString(

                            ReportSort.gcGeocode.Polygon.wStateFIPS +

                            ID_SHORTNAME);

                        strcpy(fld[i].CharData, (const char *)CState);

                        break;

                    case 9:

                        if (ReportSort.gcGeocode.bAccount)

                            fld[i].NumData = 1;

                        else

                            fld[i].NumData = 0;

                        break;

                    case 10:

                        strcpy(fld[i].CharData, ReportSort.szAccountContainer);

                        break;

                    case 11:

                        strcpy(fld[i].CharData, (const char *)

                            GetAccountString(&ReportSort.gcGeocode));

                        break;

                    case 12: // Set true if in view!

                        if (ReportSort.gcGeocode.Polygon.bInCurrentProblem)

                            fld[i].NumData = 1;

                        else

                            fld[i].NumData = 0;

                        break;

                    default: // Check for datafields (set to >= 100):

                        if (fld[i].FieldId > 99)

                        { // Finally, the datacolumns:

                            if (ReportSort.gcGeocode.fDataValue

                                [fld[i].FieldId - 100] >

                                Info.fMissingData)

                            {// Not missing data... Send true value

                                fld[i].DblData =

                                    ReportSort.gcGeocode.fDataValue

                                    [fld[i].FieldId - 100];

                            }

                            else

                            {// It's missing data. Force to zero...

                                fld[i].DblData = 0.0;

                            }

                        }

                        break;

                }

            }

        }

        if (RepRec() != 0)

        {// Something wrong!

            break;

        }

    }

    sortfile.Close();  // Close the workfile

//  Delete the workfile, as it's not needed anymore:

    unlink(szSortFileName);

    RepExit();  // Print footers and exit

    if (ReportStuff.nScreen != 0)  // Set the report destination

    {

        HourGlass(FALSE);

    }

    return;

}

The process of printing the report consists of five steps, which are easy to implement.

First you must initialize the StrRep structure. This structure contains the initialization information that the report printer uses to initialize itself. This structure is much like the report editor's StrForm structure. Here is the StrRep structure initialization:


memset(&Report, '\0', sizeof(Report));

// Save our filename and set a few other parameters:

    strcpy(Report.file, ReportStuff.szFileName);

    if (ReportStuff.nScreen == 0) // Set the report destination

        Report.device='S';

    else

    {

        Report.device='P';

        HourGlass(TRUE);

    }

    if (FormParam.width == 0 && FormParam.height == 0)

    {// Specify the window coordinates for screen. New run; no default!

        RECT    rect;

        ::GetClientRect(hWndView, &rect);

        ::ClientToScreen(hWndView, (POINT *)&rect.left);

        ::ClientToScreen(hWndView, (POINT *)&rect.right);

        Report.x = rect.left;

        Report.y = rect.top;

        Report.width = rect.right - rect.left;

        Report.height = rect.bottom - rect.top;

    }

    else

    {// Report generator ran already; have defaults

        Report.x=FormParam.x;

        Report.y=FormParam.y;

        Report.width=FormParam.width;    // Screen only (3/4)

        Report.height=FormParam.height;  // Screen only (4/4)

    }

    Report.hInst = hInst;

    Report.hParentWnd = hWnd;

After the StrRep structure is filled, a call to the report printer initialization function, RepInit(), is made. The RepInit() function returns a Boolean value of FALSE if it's successful:


if (RepInit(&Report)!=0)  // Initialize ReportEase system...

    {// This is what happens if it fails: Give error message:

        TRACE("RepInit()... failed!\n");

        return;

    }

If RepInit() is successful, the next step is to prepare the data for the report. This preparation is a multistep process. First, the data must be arranged in records. The easiest way to do this is to create a temporary work file (either in memory or as a disk file), which we will call the "sort file," to hold the records that will be created. After the work file that contains the report's records has been created, the file can be sorted if there are sort fields. After the work file has been sorted, the records can be passed to the report printing routine one record at a time. The following code fragment shows the work file creation process. A CFile object is used for the sort file:


//  For each record in VIEW, process:

    (void)GetTempFileName(0, "STR", 0, szSortFileName);

    CFile sortfile;

    TRY

    {

        sortfile.Open(szSortFileName,

            CFile::modeCreate | CFile::modeReadWrite);

    }

    CATCH( CFileException, e )

    {// No workfile, so end the report. Dump for the programmer!

        #ifdef _DEBUG

            afxDump << "File could not be opened " << e->m_cause << "\n";

        #endif

    }

    END_CATCH

After the sort file has been created, a record is added to the file for each line in the report. The following code fragment (edited to make it easier to read) shows this process. The functions GetFirstInDatabase() and GetNextInDatabase() get records from the application's master database; these records supply the necessary information for the report. The fld structure has a record for each field in the report. Your application needs to concern itself only with fields that have the SRC_APPL attribute. The SRC_APPL fields are the fields that your application will provide for the report. All other fields will be generated automatically by ReportEase.


    dwNextRecord = GetFirstInDatabase(&Info.ipSource);

    while(dwNextRecord)

    {// Now process this record:

        bUseThisRecord = TRUE; // Always use this record!

//      Add tests to see if the record may not be used here.

//      set bUseThisRecord to FALSE if the record is really

//      not usable:

        if (bUseThisRecord)

        {// This record is really wanted:

            memset(&ReportSort, '\0', sizeof(ReportSort));

            ReportSort.gcGeocode = Info.ipSource.gcGeocode;

            strcpy(ReportSort.szTerritory,

                Info.ipSource.gcTerritory.Polygon.szGeocode);

            strcpy(ReportSort.szTerritoryName,

                Info.ipSource.gcTerritory.Polygon.szGeocodeName);

            strcpy(ReportSort.szDistrict,

                Info.ipSource.gcDistrict.Polygon.szGeocode);

            strcpy(ReportSort.szDistrictName,

                Info.ipSource.gcDistrict.Polygon.szGeocodeName);

            strcpy(ReportSort.szRegion,

                Info.ipSource.gcRegion.Polygon.szGeocode);

            strcpy(ReportSort.szRegionName,

                Info.ipSource.gcRegion.Polygon.szGeocodeName);

            Info.ipSource.gcGeocode.Polygon.bInCurrentProblem =

                Info.ipSource.gcTerritory.Polygon.bInCurrentProblem;

            TRY

            {

                sortfile.Write(&ReportSort, sizeof(ReportSort));

            }

            CATCH( CFileException, e )

            {// Could not write the file... Disk full?

                #ifdef _DEBUG

                    afxDump << "Sortfile Write Error " << e->m_cause << "\n";

                #endif

            }

            END_CATCH

        }

        // Get next record (if any) in database/view:

        dwNextRecord = GetNextInDatabase(&Info.ipSource);

    }

In this code fragment, all possible field information is saved to the sort file and sorted. If the report were large, the program could have saved disk space by saving only the fields that were actually used in the report.

After the sort file has had all the records for the report written to it, it must be sorted. This is necessary only if there actually are sort fields in the report (not all reports are sorted). You can check for the presence of sort fields by using a piece of code such as the following. The StrRep member variable TotalSortFields indicates how many sort fields are present in the report.


if (Report.TotalSortFields > 0)

{

    sortfile.Close();

    SortReport(szSortFileName);

    TRY

    {// Try to reopen the sorted file:

        sortfile.Open(szSortFileName, CFile::modeReadWrite);

    }

    CATCH( CFileException, e )

    {// Could not reopen the sorted file:

        #ifdef _DEBUG

            afxDump << "File could not be opened " << e->m_cause << "\n";

        #endif

    }

    END_CATCH

}

After the records for the report have been sorted, they are then passed in order to the report printer routine. The function that accepts the records for the report is called RepRec(). Each variable in the report is contained in an array structure that is accessible through the StrRep structure. An array pointer called fld is created and is used to access this structure.


while (!nErrorOccured &&

    sortfile.Read(&ReportSort, sizeof(ReportSort)) == sizeof(ReportSort))

{

    for (i = 0; i < Report.TotalFields; i++)

    {

        if (fld[i].source == SRC_APPL)

        {

            switch(fld[i].FieldId)

            {// Need #defines here!

                case 0: // Basemap

                    strcpy(fld[i].CharData,

                        ReportSort.gcGeocode.Polygon.szGeocode);

                    break;

                case 1: // Basemap name

                    strcpy(fld[i].CharData,

                        ReportSort.gcGeocode.Polygon.szGeocodeName);

                    break;

                case 2: // Territory

                    strcpy(fld[i].CharData, ReportSort.szTerritory);

                    break;

                case 3: // Territory name

                    strcpy(fld[i].CharData, ReportSort.szTerritoryName);

                    break;

                case 4: // District

                    strcpy(fld[i].CharData, ReportSort.szDistrict);

                    break;

                case 5: // District name

                    strcpy(fld[i].CharData, ReportSort.szDistrictName);

                    break;

                case 6: // Region

                    strcpy(fld[i].CharData, ReportSort.szRegion);

                    break;

                case 7: // Region name

                    strcpy(fld[i].CharData, ReportSort.szRegionName);

                    break;

                case 8: // State

                    CState.LoadString(

                        ReportSort.gcGeocode.Polygon.wStateFIPS +

                        ID_SHORTNAME);

                    strcpy(fld[i].CharData, (const char *)Cstate);

                    break;

                case 9:

                    if (ReportSort.gcGeocode.bAccount)

                        fld[i].NumData = 1;

                    else

                        fld[i].NumData = 0;

                    break;

                case 10:

                    strcpy(fld[i].CharData, ReportSort.szAccountContainer);

                    break;

                case 11:

                    strcpy(fld[i].CharData, (const char *)

                        GetAccountString(&ReportSort.gcGeocode));

                    break;

                case 12: // Set true if in view!

                    if (ReportSort.gcGeocode.Polygon.bInCurrentProblem)

                        fld[i].NumData = 1;

                    else

                        fld[i].NumData = 0;

                    break;

                default: // Check for datafields (set to >= 100):

                    if (fld[i].FieldId > 99)

                    { // Finally, the datacolumns:

                        if (ReportSort.gcGeocode.fDataValue

                            [fld[i].FieldId - 100] >

                            Info.fMissingData)

                        {// Not missing data... Send true value

                            fld[i].DblData =

                                ReportSort.gcGeocode.fDataValue

                                [fld[i].FieldId - 100];

                        }

                        else

                        {// It's missing data. Force to zero...

                            fld[i].DblData = 0.0;

                        }

                    }

                    break;

            }

        }

    }

    if (RepRec() != 0)

    {// Something wrong! Could not add record to report!

        break;

    }

}

The process, for each record in the report, is to fill in each variable in the record (using the fld pointer, which points to an array of structures for each field in the record). After each record is built, it is passed to the RepRec() function.

As the following code shows, after each record is passed to the report printer, the report is finished by closing the sort file and calling the RepExit() function. RepExit() performs the final clean-up, such as printing final report totals. If the report is being sent to the printer, ReportEase is finished. However, if the report is being viewed in screen preview mode, ReportEase will be finished when the user closes the screen preview window.


    sortfile.Close();  // Close the workfile

//  Delete the workfile, as it's not needed anymore:

    unlink(szSortFileName);

    RepExit();   // Print footers and exit

    return;  // To caller

}

As the preceding code shows, ReportEase is designed to be integrated directly into your application. It can be used as a DLL file, or you can incorporate the ReportEase source directly into your project. One advantage of including ReportEase directly into your project is that you will have a program that is somewhat easier to debug. After you have fully debugged your program, you might want to consider placing ReportEase in a DLL. You might find that ReportEase doesn't always do things the way you want. The advantage of ReportEase's source is apparent here: You can modify the ReportEase source to make it more tightly integrated with your product. With ReportEase it's possible to create a reporting functionality in your program that is very professional and flexible, with a minimum of effort.

Using ReportSmith with Visual C++ Applications


ReportSmith works differently than ReportEase. For one thing, ReportSmith is primarily a stand-alone report generator, so it's more difficult to directly incorporate into an application.

ReportSmith uses ODBC to connect to data sources. If your database application uses ODBC to access a data source, ReportSmith can also access this data source and print reports on it.

Your application can use DDE to communicate with ReportSmith. ReportSmith will run as a DDE server and can respond to commands issued by your DDE client application.

Most of the documentation provided with ReportSmith deals with Visual Basic. Programmers using Visual C++ must try to convert the documentation and sample code provided by Borland to Visual C++-compatible code. Generally, your application must use WM_DDE_... messages to perform this communication.

At the time this book was written, the basic version of ReportSmith sold for about $100 but didn't include a runtime module. This isn't a limitation for in-house developers, but it does require your customers to own a copy of ReportSmith. The SQL version of ReportSmith, which sells for about $200 and is distributed with Borland's Delphi version 1.0, does include a distributable runtime module. This runtime module lets your users view and save a report but not edit it. The module has the same DDE links as the full version. ReportSmith is a powerful report generator whose cost isn't difficult to justify.

Figure 12.11 shows ReportSmith in report edit mode. ReportSmith doesn't have a screen print preview mode. Instead, the report edit mode lets you page through each page of the report and modify any page. This is very useful when the report is complex and the layout might not fit well on all pages. Figure 12.12 shows the second page of the report.

Figure 12.11. ReportSmith editing the first page of a report.

Figure 12.12. ReportSmith editing the second page of a report.

Summary


This chapter covered Crystal Reports, which is included with Visual C++ 4, and two other report generators, ReportEase (from Sub Systems, Inc.) and ReportSmith (from Borland). You saw examples of how to use both Crystal Reports and ReportEase.

This chapter also showed you how to add Crystal Reports to an existing Visual C++ project when using the Crystal Reports AppWizard isn't practical.

Visual C++ 4's inclusion of Crystal Reports has made the inclusion of professional-quality reporting facilities in applications much easier. However, Crystal Reports is an ODBC-based product that will accept input only from an ODBC data source.

This chapter completes Part III of this book. Up to this point, the subject matter of this book has been introductory in nature. The chapters in Part IV, "Advanced Programming with Visual C++," expand on the topics presented in Parts I, II, and III and introduce some of the more complex issues of database-application design with Visual C++, such as using OLE for interprocess communication.

Previous Page Page Top TOC Next Page