Saturday, April 12, 2014

How to use .Net visual controls in PowerBuilder Classic

Using non-visual .Net classes in PowerBuilder Classic is somewhat straightforward.  Provided the assembly (and the classes and methods within it) have been marked as COM Visible, you can run REGASM on them to create OLE registry entries and then use the class through OLE Automation.  I have a video on the SAP D&T Academy that demonstrates the process.

Using Net visual controls takes a bit more work.  There is an add-in for Visual Studio Professional called the Microsoft InteropForms Toolkit that essentially puts an ActiveX wrapper around a .Net visual user object that can contain one or more visual controls.  Some observations:

  • The toolkit is an add-in for Visual Studio.  As a result, it will only work in the Professional or higher versions of Visual Studio.  The Express versions of VB6 Visual Studio don't support add ins.  Apparently some people have found a way around the limitation, but doing so is beyond the scope of this article.
  • The toolkit only supports the VB.net language.   If you are more comfortable with C#, there are third party utilities that extend the toolkit so that you can use C# instead.  That will not be covered in this article, but you can seeInterop Forms Toolkit for C# - Home for one example.
  • The toolkit exposes Windows Forms controls.  However, you can add an ElementHost control to the toolkit control and then use that to host a WPF control.  Once again, that is beyond the scope of this article.

I have a video on the SAP D&T Academy as well that covers this technique.  This blog post will look at a different sample implementation than the one used in that video and will include the code used.

Note that the code used here was adapted from a CodeProject sample of a stand alone multi-page TIF viewer.  The code was converted from C# to VB.Net and then adapted to function as a single user object with methods rather that a stand alone viewer.

So, the first thing we're going to do is fire up Visual Studio.Net and create a new VB6 Interop UserControl.

newproject.PNG
Drag a PictureBox control over from the Toolbox onto the user object in the designer and then set the Dock property of the control to Fill.  Since we've only got one control in this user object, this will ensure that the PictureBox will always fill the entire contents of the user control when it's resized.

usercontrol.PNG
In the code editor, we're now going to declare some instance variables in the user object that we will use to track a few properties we're interested in the
"VB6 Interop Code" Region, just after the "#If COM_INTEROP_ENABLED Then" declaration.


  1.     Private _CurrPage As Integer = 0 'defining the current page (its some sort of a counter)  
  2.     Private _Opened As Boolean = False 'if an image was opened  
  3.     Private _NumPages As Integer 'the number of pages in the tiff file  
  4.     Private _FileName As String 'the name of the file that was opened  
We want a couple of those to be read only properties as well for the control, so we add those into the "VB6 Properties" Region:

  1.     Public ReadOnly Property NumberOfPages() As Integer  
  2.         Get  
  3.             Return _NumPages + 1  
  4.         End Get  
  5.     End Property  
  6.     Public ReadOnly Property CurrentPageNumber() As Integer  
  7.         Get  
  8.             Return _CurrPage + 1  
  9.         End Get  
  10.     End Property  
The reason that we're adding 1 to each of these values before returning it is because the mechanism that tracks the page numbers internal to the control is zero index based (the first page is page 0).  We need to adjust that before displaying it to the user who understands page numbers as one index based.

Finally, we're going to add some methods into the "VB6 Methods" Region, one of which is internal and the others which we'll be using from PowerBuilder to interact with the control.

The first method is the internal method that is used to refresh the image in the control when a TIF is first loaded or the user navigates to a different page:

  1.     Private Sub RefreshImage()  
  2.         Dim myBmp As Image 'a new occurrence of Image for viewing  
  3.         Dim myImg As Image 'setting the selected tiff  
  4.         myImg = System.Drawing.Image.FromFile(_FileName) 'setting the image from a file  
  5.         _NumPages = myImg.GetFrameCount(System.Drawing.Imaging.FrameDimension.Page) - 1 'the first page is 0 so we must correct the number of pages to -1  
  6.         myImg.SelectActiveFrame(System.Drawing.Imaging.FrameDimension.Page, _CurrPage) 'going to the selected page  
  7.         myBmp = New Bitmap(myImg, PictureBox1.Width, PictureBox1.Height) 'setting the new page as an image  
  8.         'Description on Bitmap(SOURCE, X,Y)  
  9.         PictureBox1.Image = myBmp 'showing the page in the pictureBox1  
  10.     End Sub  
Recreating myImg an _NumPages each time the function is called is perhaps a bit of overkill, but I wasn't interesting in trying to refactor the code, just convert it for this sample.

The following function is used to pass a filename from PowerBuilder into the control so that the control will read it and display the first page:

  1.     Public Sub OpenFile(ByVal FileName As String)  
  2.         _FileName = FileName  
  3.         _CurrPage = 0 'reseting the counter  
  4.         RefreshImage() 'refreshing and showing the new file  
  5.         _Opened = True 'a file was opened.  
  6.     End Sub  
This function is used to navigate forward one page in the TIF file:

  1.     Public Sub NextPage()  
  2.         If (_Opened) Then 'the button works if the file is opened. you could go with button.enabled  
  3.             If (_CurrPage = _NumPages) Then 'if you have reached the last page it ends here  
  4.                 'the "-1" should be there for normalizing the number of pages  
  5.                 _CurrPage = _NumPages  
  6.             Else  
  7.                 _CurrPage = _CurrPage + 1  
  8.                 RefreshImage()  
  9.             End If  
  10.         End If  
  11.     End Sub  
And finally, this function is used to navigate back one page in the TIF file:

  1.     Public Sub PriorPage()  
  2.         If (_opened) Then 'the button works if the file is opened. you could go with button.enabled  
  3.             If (_CurrPage = 0) Then 'it stops here if you reached the bottom, the first page of the tiff  
  4.                 _CurrPage = 0  
  5.             Else  
  6.                 _CurrPage = _CurrPage - 1 'if its not the first page, then go to the previous page  
  7.                 RefreshImage() 'refresh the image on the selected page  
  8.             End If  
  9.         End If  
  10.     End Sub  
That's a good start.  The user object could obviously be embellished.  For example, offering methods to navigate to a certain page directly, zoom in and out on the displayed image, print one or more pages of the image, etc. could all be added later.

Once you compile the project, Visual Studio.Net will create the registry entries that make the user object available as an ActiveX control.  Open up PowerBuilder Classic, open a window control and start to insert an OLE Control onto the window.  In the dialog that appears, select the third tab (Insert Control), and then scroll to the name of the control you created in Visual Studio.net.  In my sample, I give it the rather unimaginative name of "TiffViewerControl.TiffViewerControl"

insertOLEControl.PNG

In the PowerBuilder window, I added buttons to open a TIF file and display it in the control, to move forward one page and to move backwards one page.  I also have a static text field that display the current page number and number of pages in the document.

Note that the functions and properties of the user object do not display in the PowerBuilder IDE.  You end up calling them as methods and properties of the ole control object attribute.  PowerBuilder compiles that without question and only attempts to validate that the references are valid when the code is run.

The script in the button that opens a TIF file for display is as follows:

  1. Integer li_rc  
  2. String ls_pathname, ls_filename  
  3. li_rc = GetFileOpenName ( "Select a TIFF file", ls_pathname, ls_filename, "TIF", "TIF Files (*.tif),*.tif" )  
  4. IF li_rc = 1 THEN  
  5.   ole_1.Object.OpenFile ( ls_pathname )  
  6.   of_displaylocation()  
  7. END IF  
To move forward one page:

  1. ole_1.Object.NextPage()  
  2. of_displaylocation()  
To move backwards one page:

  1. ole_1.Object.PriorPage()  
  2. of_displaylocation()  
The of_displaylocation function called by all three scripts is the one that populates the static text field with the current and total page numbers:

  1. integer  li_currpage  
  2. integer  li_numpages  
  3. li_currpage = ole_1.Object.CurrentPageNumber  
  4. li_numpages = ole_1.Object.NumberOfPages  
  5. st_location.text = "Page " + String ( li_currpage ) + " of " + String ( li_numpages )  
Finally, I have some code in the resize event of the window so that the user object resizes when the window is resized.

  1. ole_1.Resize ( newwidth - 150, newheight - 250 )  
With that, we're done.  Run the app, load up a TIF file and start browsing through it.  For this sample, I converted the 142 page PowerBuilder ORCA manual from PDF to TIF.

tifviewer.PNG

The sample code, both for Visual Studio.Net and for PowerBuilder 12.5 Classic, is available on my Google Drive.

No comments: