Photo by PopCultureGeek.com
Early in my career I created a custom document storage solution as part of several of my applications. This led to the Clarion Imaging Templates and later the Imageman Templates, both using third party OCX’s. Recently one of those customers wanted some enhancements to their application, and I decided I would rather spend a couple of days converting the application into WinDev, than adding features in C6 that are built into WinDev. My first step was to explore the possibilities of using WinDev native functions to create a document viewer instead of using a third party library. I was pleasantly surprised at how easy it was and how much functionality was supported directly by WinDev. The resulting Document Viewer is very light weight and handles most of the normal requirements, its not as powerful as I would like, put it will do for this project until I create a Document Viewer using a third party .net library at some point in the future. At the end of the article you will find a download link for the Component and the source code.
Let’s establish the requirements for this project. It must be able to handle multiple page document files (TIF or PDF), it should be a stand-alone component that can be incorporated into any of my projects, to keep cost and complexity down it will only use Wlanguage native functions. This article will explain the Wlanguage features I used, how to create and use a component, using a floating toolbar, some specific issues with printing multiple page image files and my work around, as well as some issue I ran into with window position storing functions and how I resolved those.
WinDev ships with a TIF viewer demo and I used that as a starting point for the project, but there were several issues with it that kept if from fitting my needs. So let’s get started!
We start with a window, that has a floating toolbar and an image control that is using all the space of the window:
We set the display mode for the image to Homothetic without increasing size and High Quality, position to Top Left, and anchoring to grow both in height and width. This assures that the document will always fit the screen when first opened, show in High Quality mode, and be positioned at the Top Left corner of the viewer window, and the image control will always fill the size of the window even when the window is resized.
We need to add a Floating Toolbar to the Window, we will go over adding the buttons as we go along. Use the Insert->Toolbar menu to create the toolbar. Set the position to Floating, and we also set the authorized positions to only floating as we do not want this to be a dockable toolbar.
Now that we have the general house keeping done let’s get to the coding! First we want to pass the name of the document to display to this procedure, remember we want this to be a stand-alone viewer. So we go to the code of our window and place the following line of code in the Global Declarations:
PROCEDURE WIN_TiffView(gsCurrentFileName is string = "C:My ProjectsDocumentViewerExeLoremIpsum.tif")
Our window name is Win_TiffView (which is also our procedure name in WinDev), all we are doing is creating one parameter for our procedure, gsCurrentFilename, which is a string and has a default value of “C:My ProjectsDocumentViewerExeLoremIpsum.tif”. Now when we open the window we can pass in the name of the document file we want to display. I gave it a default value as it made it easier for me when I was testing the procedure.
Next we have the following code in the Initialization section of the Window:
IF gsCurrentFileName <> "" THEN InitImage(gsCurrentFileName) END RestoreWindowPosition() sStoredValue is string sStoredValue = RegistryQueryValue(ProjectInfo(piRegistry)+ "" + WinInput() + "TBAR_Navigation","PositionOuverture") IF sStoredValue <> "" THEN TBAR_Navigation..X = ExtractString(sStoredValue,1,",") TBAR_Navigation..Y = ExtractString(sStoredValue,2,",") END
The first if statement is making sure that we did have a file name passed and if we do it calls our InitImage Local procedure. The rest of the code deals with some issues I ran into with the standard WinDev window position restore logic. We will tackle the window restore code at the end of the article. So for now lets look at the InitImage local procedure:
PROCEDURE InitImage(DocumentFileName is string) gsCurrentFileName = DocumentFileName IF gsCurrentFileName <> "" THEN IMG_View = gsCurrentFileName InitTittleBarandPageNumber() TBAR_Navigation..Visible = True IMG_View..Zoom = zoomAdaptSize END
gsCurrentFileName is our parameter that we passed in, which also makes it a global variable, so why are we setting it to the DocumentFileName that was passed into the InitImage procedure? As we will see later we want to be able to change the document file that is being displayed without closing and reopening the window, so we will be calling the InitImage procedure from elsewhere to change the document name. This line of code makes sure that we get the correct image name in our global variable.
The IF statement is just my typical belt and suspenders way of coding. The next line sets the image control to the document file passed, this is how the image control knows which file to display. Next we call another local procedure InitTittleBarandPageNumber which is used to do some house keeping on the toolbar and the window tittle bar, we will look at that code in more detail in shortly. The next line makes sure that the toolbar is visible.
So does the final line do? Its sets the zoom level of the image. When I started this project, I used the properties of the image, and the window to figure out the correct ratio’s and did all the math to figure out what width and height to set the image control so that it would fit the window. Then I remembered what has become the Mantra of our Skype WinDev group “If it seems hard, you are probably doing it wrong!” and sure enough after some investigation in the help files, I found that WinDev has some special constants for the zoom property to make it fit the screen, fit width or fit height. So all I have to do is set the Zoom to “ZoomAdaptSize” and the document will be resized to fit the current size of the image control.
So let’s take a look at the code in the InitTittleBarandPageNumber local procedure:
PROCEDURE InitTittleBarandPageNumber() IF gsCurrentFileName = "" THEN MyWindow..Caption = "Document Viewer" RETURN END //Display the page number and the total number of pages found in the image file MyWindow..Caption = "Document Viewer - " + gsCurrentFileName + ": "+ StringBuild("%1/%2 pages",IMG_View..PageNumber, IMG_View..NumberPage) BTN_PGDN..State = Grayed BTN_PGUP..State = Grayed IF IMG_View..PageNumber > 1 THEN BTN_PGDN..State = Active END IF IMG_View..PageNumber < IMG_View..NumberPage THEN BTN_PGUP..State = Active END EDT_PageNumber = IMG_View..PageNumber
The first IF statement checks that we have a filename and if we do not then it sets the tittle bar of the Window to “Document Viewer” and exits the procedure. If we do have a document name, then the next line changes the tittle bar to show the document name and which page is being displayed, and the total number of pages in the document. The “PageNumber” property of the image control contains the current page being displayed and the “NumberPage” property contains the total number of pages of the image. Not the best naming convention and I predict that like me you will probably mix that up a few times before it sticks. The StringBuild function is a very handy string function of WinDev that makes it easy to build strings containing several variables, and I strongly suggest you learn more about it via the help files.
Next we disable both the Page Up and Page Down buttons and then reenable them based on what page of the document we are currently displaying and how many pages are in the document. And then finally we update the edit control with the current page number displayed.
All that’s left for our viewer window is to add some buttons to our toolbar and place some code on them. Adding controls to the toolbar is like adding them to a window, except the controls automatically position themselves, it takes a little getting use to but once you use it for a few minutes, it makes sense. If you struggle with it, leave a comment and I will be glad to try to help you over the hurtle.
So what features are we going to have on the toolbar? Printing, Page Up, Page Down, displaying the current page and allowing the user to jump to any page, zoom in, zoom out, fit to screen, fit to width, fit to height, and close the viewer window. In an ideal world I would also like to include rotate left and right functions. Unfortunately, although version 16 has added Rotate functions for images, they don’t seem to work reliably with multiple page image files. There is also a demo app that uses the Windows API to rotate an image file, but again this does not seem to work reliably with multiple page images. This is one of the main reason that I will likely have to recreate this viewer at some point in the future using a third-party .net library.
Let’s skip the printing button for now and return to it later. So lets take a look at the Page Up and Down buttons.
The Page Up Code
IF IMG_View..PageNumber < IMG_View..NumberPage THEN IMG_View..PageNumber ++ InitTittleBarandPageNumber() END
The Page Down Code
IF IMG_View..PageNumber > 1 THEN IMG_View..PageNumber -- InitTittleBarandPageNumber() END
The code behind these buttons is very similar. The IF statement makes sure that we don’t go lower than page 1 or higher than the total number of pages in the document. The ++ and — are short hand syntax used in WinDev to subtract 1 or add 1 to the variable. It is the same as the statement: Variable = Varible + 1. The “PageNumber” property as you remember tells use the current page displayed, by changing this property we change the page being displayed. We also make a call to the InitTittleBarandPageNumber local procedure to do the housekeeping on the Window Tittle bar and toolbar controls.
By using an edit control to display the current page number, not only can we display the page number, but we can also allow the user to type in a page number and jump to that page. To accomplish that we place the following code for the edit control
IF EDT_PageNumber > 0 AND EDT_PageNumber InitTittleBarandPageNumber() END
Again the IF statement makes sure that the user does not enter an invalid page number. As long as they have entered a valid page number, we change the PageNumber property to the value entered and call the InitTittleBarandPageNumber local procedure to do the housekeeping.
The Zooming code is all very simple code, consisting of one line each.
Zoom In Code
IMG_View..Zoom = IMG_View..Zoom * 1.5
Zoom Out Code
IMG_View..Zoom = IMG_View..Zoom / 1.5
Fit to Screen Code
IMG_View..Zoom = zoomAdaptSize
Fit to Width Code
IMG_View..Zoom = zoomAdaptWidth
Fit to Height Code
IMG_View..Zoom = zoomAdaptHeight
All of these use the “Zoom” property. The Zoom In and Out function simple increase or decrease the zoom by a factor of 50% and all three of the Fit buttons use the special WinDev constants that we already discussed to display the image as desired. Again I can not emphasize enough how incredible these special constants are! The code to do all the math and figure out the ratio’s is at least 20 lines of code and takes a while to work out, trust me I did it before reading the help files and finding these nifty shortcuts!!
And finally the close button, calls one line of code to close the viewer window.
So let’s discuss the Print Function. The example application that comes with WinDev creates a JPG file for each page of the document and then prints them directly. Regardless of the format chosen (JPG, BMP, etc) the quality of the document is reduced which would not work for my needs. So instead of using direct printing I am going to use a report. WinDev has the ability to use a PDF image as the background image of the report and chose which page to print, so we can use that ability to print our image. This still left me with a few issues. First my documents are stored in TIF format not PDF, and WinDev does not support using a multiple page TIF file as the background for a report, regardless of the pagenumber property it always prints page 1.
The other issue I ran into is that WinDev really intended for the multiple page PDF to be used as a report background when printing forms with the data overlaid. This means that they intended for you to set the PDF at design time not runtime, I got a few odd results because of this, so i got around this by creating a blank 2 page PDF file and using this as the background of the report.
So lets look at the code behind our print button first then we will move on to the Report procedure itself.
ImageInfo, Format are strings ImageInfo = BitmapInfo(gsCurrentFileName) Format = ExtractString(ImageInfo, 1) IF Format = "TIF" THEN fDelete("printtif.pdf") IF NOT ExeRun("""" + fCurrentDir() +[""]+ "Tiff2PDF.exe"" -o printtif.pdf -p A4 """ + gsCurrentFileName + """",exeIconize,exeWait) THEN Error(ErrorInfo()) END iPreview(iPage,"Document Print") iPrintReport(RPT_PrintImage,fCurrentDir() +[""]+ "printtif.pdf",IMG_View..NumberPage) fDelete("printtif.pdf") ELSE iPreview(iPage,"Document Print") iPrintReport(RPT_PrintImage,gsCurrentFileName,IMG_View..NumberPage) END
The BitmapInfo function returns a tab delimited string containing several pieces of information about an image file. The first piece of information is the Format of the Image. We use the ExtractString function to return the value in position 1 of the delimited string. ExtractString is another one of those handy WinDev functions that I strongly encorage you to read up on, it allows you to pull a value from a delimited string from any position in the list. The character used to delimited the string can be set (i.e. Tab,Comma,Semi-Colon,etc).
The IF statement test if the format of our document is TIF, if it is then we first delete the file “printtif.pdf” from the disk. Again this is my Belt and Suspenders style of coding, I just want to make sure the image has been deleted so it won’t give us any trouble in the next statement.
The next statement is calling a command line utility to convert a TIF image into a PDF image and naming the converted file “printtif.pdf”. EXERUN calls an external EXE and returns an error code if the command could not be ran. The fCurrentDir() function returns the current path of the application, which unless it has been changed elsewhere is where the .EXE is located. The rest is just building the string for the actual call to the command line utility. exeIconize and exeWait, tell WinDev that we would like the command to not open as a window, it will just be an icon on the taskbar, and that we want to halt execution of the application until the command line function finishes.
Next we setup the report to be displayed in the print preview and call our Report procedure with the name of the file and the number of pages in the document. In this case we pass in the “printtif.pdf” file name that we just created via the utility. We pass the total number of pages, because WinDev does not expose the “NumberPage” property for the background image of the report, so we get it from the property of the image control and pass it to the report. And then finally we delete the “printtif.pdf” file yet again, what can I say Belt and Suspenders has never failed me!
If our document is already in PDF format we can skip all the extra steps and just setup the report preview and call our Report procedure with the name of the document file and the number of pages.
Print Document Procedure
For our report procedure we create a report with no margins and just a body block. We set the background of the Report to the blank PDF mentioned earlier, and set it to use page 1.
In the opening code of the Report we redefine the procedure to accept the two parameters that we are going to pass (the Document File Name and Number of Pages), we also change the background image to the document file passed. You should notice that although we set the background image on the Report properties, the run-time property is actually associated with the Body block.
PROCEDURE RPT_PrintImage(ThisReportImage is string,gnPages is int) RPT_PrintImage.BODY..Image = ThisReportImage
At this point our report would run and print our first page. In order to print the remaining pages of our document we add the following code to the Closing section of the Report
IF gnPages > 1 THEN nCount is int HourGlass(True) FOR nCount = 2 TO gnPages RPT_PrintImage..PageNumber = nCount iPrintBlock(BODY) END HourGlass(False) END
The IF statement makes sure that we have multiple pages. If we do have multiple pages, we set the cursor to an hourglass, and loop from 2 to the total number of pages, changing the “PageNumber” property and reprinting the body block. Once completed we return the cursor to its normal state.
Global Procedure to Manage the Viewer Window
We now have both our viewer window and report and we are almost finished. Since we want to have the ability for the viewer window to stay open as we call it with different document files, we are going to create a global procedure to manage the call to the viewer window.
PROCEDURE DisplayDocument(DocumentFileName is string) IF WinStatus(WIN_TiffView) = NotFound OpenSister(WIN_TiffView,DocumentFileName) ELSE WIN_TiffView.InitImage(DocumentFileName) END
This procedure accepts the document file name as a parameter. The IF statement checks to see if our Window is already open. If the window is not open it is opened using the OpenSister Function, passing the document file name as a parameter. The OpenSister function opens a non-modal Window that can be interacted with separately from the current window. This allows us to have our document viewer open on a second monitor and display the related document as we scroll through a list of documents displayed in our main window on the first monitor. If the Window is already open we call the InitImage local procedure of the window to change the image displayed. By using the context WindowName.LocalProcedureName we can call the local procedure of a window from elsewhere in the code, we can also perform actions on the controls of another window using the WindowName.ControlName syntax. Once mastered this ability opens up a world of possibilities for a WinDev developer.
Technically in today’s world I should have created this as a class instead of a global procedure, but its a simple one line procedure call and let’s face it I am an old school guy that goes for the simple and obvious solution when available.
Storing and Restoring the Window Position
WinDev offers the functionality to store the window position and size and then restore it at run-time by simply selecting a few options on the windows properties. This allows your application to remember the position and size of a window if the end-user resizes or moves the window. This seems to work well, unless the end-user is using dual monitors. For a document storage and viewing application, dual monitors are very helpful and allow you to have the viewer open on one monitor while working with the data on the other. Using the standard WinDev functions, my viewer window was not remembering to open on the secondary monitor. After some experimenting I created my own functions for storing the windows position and restoring it. The code is included in the component and source code of this project, I will be writing a separate blog article in the next few days explaining the code . That article will be located on the blog at http://www.thenextage.com/wordpress/
Creating a Demo procedure and executeable
In order to demo and test the viewer I create a simple procedure with few buttons on it to view documents and close the viewer. Nothing special there and I won’t go over the code, although it is included in the project for your use. At this point you can create an executable for the project the same as you would any other project. Again I won’t go in to the steps of how to create an executable. So go ahead and create the executable and play around with the interface. Once your done come on back and we will create a component.
Creating a Component
So now we have our handy viewer procedure and report and we could simply copy this into every project we need to use it in. But, what happens when we decide to add a feature to our document viewer? We would have to modify the procedure in every project. To avoid this issue we will create a Component. This is very similar to a .DLL, except it allows us to include documentation and store our component in the SCM so that it will automatically be updated in all of our projects when it is changed.
WinDev allows you to create multiple compilation configurations for one project. To get started use the Workshop->External Component->Define a new component from this project.
After the splash screen, the next screen will ask you for the Name and the Description of the component.
The next screen wants to know if our component is only used by WinDev and the target operating system.
The next screen want’s to know which elements we need to include in the component. Since we do not need the Win_Demo window we will uncheck it.
Next you get an “It’s Done” window, followed by the first screen of the generate component wizard. Skip both screens, and you get another Elements to Include screen, but notice this time that the Win_Demo window is not included in the list.
The next screen prompts for accessible elements, meaning which procedures are allowed to be called by programs using this component. As you see we are only exposing the report, viewer window and our global procedure. Notice that you expose a set of global procedures at once, if we only want to expose a particular global procedure we could create it in a separate set of global procedures and only expose that set.
The next two windows are for multilingual features so we will skip them. The next allows us to set a version number and allows us to keep track of any backward compatible version number if needed. We will just take the defaults here as well. The next screen allows us to include owner information for the component.
The next screen prompts for an image to be used by the component when it is displayed in the component pane.
The next screen prompts for overview information to be displayed in the component pane.
The next screen lets you know that documentation for the component is about to be generated, once we skip this screen we see the documentation that was automatically generated and can edit it as needed. This documentation will be included with your component so you should enhance it as needed to make it as easy as possible for other developers to use your component.
The next screen allows you to create a help system for the component, we will skip that step. The next is for User Macro-Code and we will skip it as well. Next, we reach a screen that wants to know if we want to publish the component to the SCM. By doing this it allows other developers and ourselves to retrieve the component from the SCM to use in other projects and anytime the version stored in the SCM is updated, the version used by the project will also be updated.
The next screen is for backups and we will skip it, and then finally we reach the “It’s Done” window and can create the component.
Next, you will get a couple of screens about checking things in and out of the SCM. Answer the prompts appropriately and you have created the component and included it in your SCM to be used by other projects and developers.
Using the component in another project
Now that we have created our component and added it to the SCM, its time to use it in another project. So we open the project that we want to use it in. For example purposes I want to include the document viewer in a project that display’s a list of invoice records and allows the user to view the scanned version of the image if they wish.
After opening the project we have to include the component. Use the Workshop->External Component->Import a Component into the project->From SCM menu option. After answering the prompts about where your SCM is located you will be able to view a list of the components in the SCM. After choosing the DocumentViewer component, we see the documentation for the component. This documentation should look familiar to you, its the documentation we edited earlier when creating the component. You can view the documentation anytime via the project explorer, by right clicking on the component and choosing description.
Once completed we can see the component in the project explorer under external components as well as the accessible elements of the component. This list corresponds to the list that we chose when we created the component.
Now its just a mater of using the component in our project. In our table I simply place the following code in the Row Display for the Table:
ImageFileName is string IF WinStatus(WIN_TiffView) <> NotFound THEN IF TABLE_WIN_Inquiry.COL_HstInvHdrSysId > 999999 THEN ImageFileName = NumToString(TABLE_WIN_Inquiry.COL_HstInvHdrSysId/1000, "016.3f") ELSE ImageFileName = NumToString(TABLE_WIN_Inquiry.COL_HstInvHdrSysId/1000, "012.3f") END DocumentViewer.DisplayDocument("C:temp" + ImageFileName) END
This code will execute anytime a user selects a new record in the table. If the ImageViewer has not been opened then we don’t execute the code. This allows the user to only display images when they wish. The rest of the code is just some magic I use to create the file names based on the sysid of the records. And then last line is the actual call to our DocumentViwer global procedure to display the Image.
Now we add a button to the toolbar for the window to allow the user to toggle the viewer on or off. Using the following code:
IF WinStatus(WIN_TiffView) = NotFound THEN IF TABLE_WIN_Inquiry.COL_HstInvHdrSysId > 999999 THEN ImageFileName = NumToString(TABLE_WIN_Inquiry.COL_HstInvHdrSysId/1000, "016.3f") ELSE ImageFileName = NumToString(TABLE_WIN_Inquiry.COL_HstInvHdrSysId/1000, "012.3f") END DocumentViewer.DisplayDocument("C:temp" + ImageFileName) ELSE Close(WIN_TiffView) END
This code is very similar to the last code, except if the window is not open then we do the work to determine the file name and open the window. If the window is currently open this we close the window.
You now have a simple document viewer component that can be used in any project by simply including the component and making the appropriate calls. If you would like to download the component it is located at http://www.thenextage.com/downloads/DocumentViewerComponent.zip and if you want the entire project so you can tinker with the code its located at http://www.thenextage.com/downloads/documentviewerproject.zip. Just be sure to share any improvements you make with the rest of us.
There is also a webinar that I did for our Skype Windev group that can be viewed at http://www.wxlive.us