Keeping a Leash on Crystal
by Ross Mack - GUI Computing
With Visual Basic 3.0 Microsoft decided to bundle Crystal Reports with VB to give developers a head start in producing reports on their databases. Crystal is a powerful tool and most every Visual Basic developer has used it for at leas a couple of projects. However, a common complaint is that when creating a report preview Crystal generates a separate window over which the programmer has some control, in the way that it first appears, but is otherwise owned solely by Crystal. Users can often be confused when the Crystal report window appears over their application and they can't get back to the app, or end up with a bunch of errant report windows lying around after they have closed down the application itself. Worse still, these windows consume memory to maintain the report that they are displaying.
A while ago I discovered a way around this, and I have been using it ever since. For anyone who is not aware of this technique, you might just find it useful next time you are including Crystal reports with your application.
Essentially what I do is to instruct Crystal to produce a report window inside one of my windows instead of creating its own window. I then own the window that the report is displayed in and I can dispose of it, move it, minimise or maximise it as I like, all quite simply in code.
The first thing you need is a simple window, like the following. I have a standard one I keep around that I just add to a project when I need it.
As you can see the form is blank, it needs no controls as Crystal will dynamically create the preview and the preview's toolbar dynamically onto the window at runtime. You might also notice that this window is an MDIChild Window. This is because I often like to use an MDI form as a basis for reporting in my applications. It allows the user to create a number of reports and manipulate the windows easily, maintain them in a defined area (as opposed to SDI windows), and view reports side by side with ease just by using the inbuilt functionality of MDI windows.
There is also only a small amount of code within the form.
Sub RedrawChild ()
Dim hWndChild As Integer
hWndChild = GetWindow(Me.hWnd, GW_CHILD)
If (hWndChild <> 0) Then
MoveWindow hWndChild, Me.ScaleLeft, Me.ScaleTop, Me.ScaleWidth, Me.ScaleHeight, 1
End If
End Sub
Sub Form_Activate ()
RedrawChild
End Sub
Sub Form_Resize ()
RedrawChild
End Sub
The code requires the following API declarations:
Declare Function GetWindow Lib "User" (ByVal hWnd As Integer, ByVal code As Integer) As Integer Declare Sub MoveWindow Lib "User" (ByVal hWnd As Integer, ByVal l As Integer, ByVal t As Integer, ByVal w As Integer, ByVal h As Integer, ByVal redraw As Integer) Global Const GW_CHILD = 5
As you can see we have one general function and two calls to it. All the function does is use an API to retrieve the handle of the first child window of the form. When a report is created this will become the Crystal report window (which is created as a child of this form). If it manages to retrieve a valid Window Handle (hWnd) it then moves the child window so that it fills the entire client area of the form. This means that as the form is resized the Crystal preview window will always fill it.
The end result (at runtime) is a window that looks like this:
Again, this example is an MDI child window so I have shown it as it appears within its MDI parent. Note that everything that appears on the form now (excluding the border and title bar) is created dynamically by Crystal just as it would normally create them in it's own windows.
So, the next question is, how do we get Crystal to create a report on our window?
I use a simple bit of code, of which the following is sort of a minimalist version. It assumes that your reports will not need additional formulas set or additional Record Selection Criteria.
Sub DoCrystalPreview (ByVal sTitle_IN As String, ByVal sRPTName_IN As String)
' Dimension a new instance of the report preview window
Dim frmPrev As New frmChild
Dim sMsg As String
Dim iResult As Integer
Busy ' hourglass stuff
' Set the caption of the preview window
frmPrev.Caption = sTitle_IN
' Tell Crystal what report to print
Crystal.ReportFileName = sRPTName_IN
' WindowParentHandle tells Crystal to print inside the
' window we specify by passing it's hWnd
Crystal.WindowParentHandle = frmPrev.hWnd
' Do the report
iResult = Crystal.PrintReport
UnBusy
' Check for and report errors
If iResult <> 0 Then
MsgBox "Error Encountered Printing Report: " & sTitle_IN & CRLF(2) & "ERROR " & Crystal.LastErrorNumber & ": " & Crystal.LastErrorString, 48, "Report Error"
End If
End Sub
The comments should help describe what is going on. What happens first is that we create a new instance of the previewing window (the one I discussed above) so that we can print to it. We do this instead of just referring to the window as normal and loading it as normal because we may want to have more than one instance of this window (with reports on each) active at the same time. The we give Crystal enough information about the report for it to produce it, in this case the RPT filename and a title. Then we set the WindowParentHandle Property. This tells Crystal that instead of creating its own window we want it to create a report preview as a child of the window we have specified. We then check for errors and such and we are finished.
So, that's it. There is not much involved, but it's a trick that can be very useful and save yourself and your users a bit of grief. There is a simple sample application available (VB 3) for download with a couple of simple reports that run off the old favourite BIBLIO.MDB (JET 2.0).