|By Buck Woolley||
|May 1, 2003 12:00 AM EDT||
The PowerBuilder DataWindow object is one of the main reasons for the success that PowerBuilder has achieved as a software development tool. Together, they have matured and developed. However, since the beginning the DataWindow has been used to display and manipulate data in the form of lists and reports as well as in various data input and maintenance forms.
Today, users are demanding a more dynamic user interface in which intuitive display and manipulation of graphical representation of data replaces the simple method of displaying and editing text data. The PowerBuilder DataWindow object is ideal for developing a wide variety of graphically rich dynamic user interfaces. Although the DataWindow is limited in the types of graphics primatives that can be generated, its tight coupling of data to the properties of the objects that can be created makes manipulation of all visual DataWindow objects a simple and robust process.
This article covers some of the primary methods used to generate rich graphic user interfaces. The methods covered include drawing simple shapes, moving images within a DataWindow and from one DataWindow to another, and using metadata to control large numbers of graphic objects on a DataWindow. The techniques discussed cover only the presentation aspects of the DataWindow and are normally used on external DataWindows. Therefore, there is no discussion regarding database access SQL syntax. Code examples come from DataWindow-based versions of the simple video games, "Minesweep" and "Breakout." You can download these examples from www.dw-extreme.com/download8.0.htm.
Drawing Simple Shapes
Although the DataWindow doesn't support the large variety of shapes and graphic primitives available on even the simplest CAD program, it does support enough to provide some interesting and useful graphics capabilities. These include the line, ellipse, rectangle, and round rectangle objects seen in Figure 1.
One useful functionality that can be applied to the DataWindow using the rectangle object is the ability to draw a rectangle with a mouse and capture or select objects within the boundaries of the rectangle. After the objects are selected, the rectangle is removed. This is similar to the ability to select objects on the Windows desktop using the mouse. You can also use this method to draw rectangles, ellipses, and round rectangles by not doing the step that removes the rendered object.
Creating and Manipulating a Rectangle
Creating and manipulating the rectangle requires the creation of three mapped custom events on the DataWindow:
- uo_lbd mapped to pbm_lbuttondown
- uo_mm mapped to pbm_mousemove
- uo_lbu mapped to pbm_lbuttonup
These events will contain all the code required to generate, manipulate, and destroy the rectangle along with object selection. Either place the code directly on the DataWindow control or, for a more object-oriented approach, create a custom nonvisual object that contains a reference to the DataWindow. You can then create custom events in the nonvisual object that map to the DataWindow events.
If you're creating a rectangle to render permanently on the DataWindow, you probably want to generate a unique name for that object. If you're creating a rectangle for the purpose of selecting objects on a DataWindow, use a static name, in this case, "size_rect" as in Listing 1 (code listings for this article are available at www.sys-con.com/pbdj/sourcec.cfm). The only required parameters xpos and ypos are provided by pbm_lbuttondown. To make sure a "size_rect" doesn't already exist, issue a destroy statement before you create the new one. This may happen if you do the uo_lbd event, then move the mouse off the application and lift your finger off the mouse. You will have created a rectangle without ever destroying it, so it is a good idea to destroy it before creating a new one. You want to store the original x and y locations of your pointer in instance variables il_x and il_y.
This event controls the rendering of the rectangle on the screen as you move the mouse. First check if the left mouse button is down during the mouse move, by checking if flags = 1. The only trick here is that as long as your pointer is in an area that is greater than the original x and y locations, you're simply adjusting the width and height of the rectangle. If you move your pointer into an area that's less than the original x or y, you must make adjustments to the x, y, width, and height parameters of the rectangle as shown in Listing 2.
Depending on what you are creating the rectangle for, you'll do one of two things in the uo_lbu event. If you're drawing multiple graphic objects in the DataWindow, it's a good idea to store in the uo_lbu all the data associated with the objects in a properties datastore. The datastore would contain data such as the object's name, x, y, height, width, line types, and color properties. The unique name you generated for the object in the uo_lbd event provides an easy method for finding objects on the properties datastore. This will be useful later in the application because you may need to access the object's properties; it's much easier to look them up with the DataWindow find function using the object unique name than to search for them by looping through the properties using the describe function. This capability is useful when filtering for the existence of objects within a spatial boundry such as a selection rectangle. As mentioned earlier, you can use this method to create rectangles, ellipses, and round rectangles. Lines can also be created this way except that the line object has the x1, y1, x2, and y2 properties instead of x, y, width, and height to describe its position.
If you're drawing a selection rectangle you want to save the x, y, width, and height properties of the rectangle into variables. These will be used later to find the objects within the boundary of the selection rectangle. The last item needed is to issue a idw.modify('destroy size_rect') to remove the selection rectangle from the DataWindow.
Selecting the Objects
If you have rendered objects such as images on the DataWindow it's a good idea to have a datastore to hold the objects and their properties in storage. The properties of interest are the object's x, y, width, and height properties. PowerBuilder provides a powerful tool for selecting the object enclosed by the rectangle, the setfilter() function shown in Listing 3. In the uo_lbu event filter the datastore contains rendered objects with this setfilter argument.
This leaves a list of objects that are selected by your rectangle. The final step is to show the user a visual indicator that the objects have been selected. Once again PowerBuilder provides a good solution that will work with any image. That solution is the DataWindow's image object.invert property as seen in Listing 4. This will display all selected images in an inverted appearance. The easiest method is to loop through the selected objects and build a modify string that will invert all selected objects in one modify statement.
Listing 4 also shows that the IDs of the selected objects are stored in an instance array to be used later for other processing and to reset the selected objects to a normal appearance. This is usually done in a loop in another event after the selected objects have been processed (see Listing 5).
Moving Images Within and Between DataWindows
Another basic functionality required for an interactive graphical interface is the ability to move images, text, or graphic primitives within a DataWindow and between DataWindows using the mouse. Of course, PowerBuilder provides an easy solution for moving objects within the DataWindow, the movable property. Just set the movable property to 1 dw_1.object.rect.movable=1 and the object can be moved by using the mouse. What you see is an outline of the object linked to your mouse. The object actually moves to its new location when you release the left mouse button. Sometimes, however, users may want the object and not just an outline of the object to follow the path of the mouse as they move it. Although this cannot be accomplished by simply setting a property value, it's easy to do.
Once again moving objects relies on the three important custom DataWindow events used to respond to mouse actions.
When the user clicks on an object for the purpose of moving it, several things must be done. First, you must record the identity of the object that was clicked on and store it in an instance variable. Use the getobjectatpointer() function to get the clicked object. You also want to store in instance variables the object's x and y properties. The uo_lbd event contains the code shown in Listing 6.
To link the object to the movement of the mouse, adjust the x and y properties of the object in the uo_mm event. This is done by simply applying the difference between the object's original location and the current position of the pointer:
IF flags = 1 THEN
idw.modify(is_object+'.x='+STRING(xpos - il_x)+
' '+is_object+'.y='+STRING(ypos - il_y))
Upon releasing the mouse in the oe_lbu event, reset the is_object instance variable so the object won't move the next time you click the mouse button:
is_object = ''
Moving Images from One DataWindow to Another
In many applications you might have a situation in which you want to drag objects from a palette of objects to some type of canvas. In this case, you'll have a window that might have one DataWindow that contains a set of standard images that you can select from and copy to another DataWindow in which you build something like a network, pipeline, or workflow diagram. To make the movement look seamless between the two DataWindows, use a third independent object that performs the actual move between the objects - in this case, a descendant of the PowerBuilder picture object. A descendant is used because you'll create instance variables within the picture object to store some offset values determined by the location of the pointer when you selected the object to move. You also must set the picture object to not be visible because you're using it as a drag object only.
The action is initiated by the oe_lbd event on an object in the palette DataWindow. In this event, extract the row of the selected image and the image name. You also want to position the picture object precisely over the DataWindow image object and have it assume the width and height of the object so that it appears as if you are dragging the actual image object. This is made more difficult if you have autosize set on your detail band. In the palette DataWindow you must create a computed field that contains the cumulative y value of every row. This is done with the following computed expression called row_height:
cumulativeSum( rowheight() for all)
Also take into account that the user might have scrolled down and the first row on the page is not row one. This is resolved by using the handy PowerBuilder property FirstRowOnPage to determine which row is the first visible row. The code in the oe_lbd event shown in Listing 7 takes care of this.
At this point you should see a box perfectly outlining the boundary of the DataWindow image object. With your mouse you should also be able to move the box around the window. If you drop the dragobject on the canvas DataWindow, you'll process the dragobject called "source" in the dragdrop event of the canvas DataWindow. The dragobject is assigned to a variable of type uo_pic, the source of the dragobject. The il_x and il_y hold offset values so the object will be created with the same reference to the mouse pointer as it had when it was selected. The dimensions of the created bitmap come from the dimensions of the dragobject. Listing 8 shows how to create the new bitmap on the target DataWindow.
Figure 2 shows the image on the canvas DataWindow at the location of the mouse cursor with the same bitmap and properties as the selected image on the palette DataWindow.
Select an image from a palette DataWindow. Note the rectangle around the selected object, which is the picture object with its visibility set to 0 and after the drag(begin!) function is invoked.
Drag the object from the palette DataWindow to the target DataWindow.
Drop the object onto the target DataWindow and build the DataWindow bitmap object based on the properties stored in the draggedobject and the results of the pointerX() and pointerY() functions.
Rich Graphical DataWindows Using Metadata
Although the DataWindow can only do very basic drawing functions, take advantage of its row based architecture to develop very advanced interactive graphical user interfaces. Examples of objects that can be developed using Metadata DataWindows include not only the minesweep and breakout games but also interfaces for business applications such as a project planner/scheduling object, appointment object, advanced charting, and a hierarchical chart object for uses such as an organizational chart.
What controls the functionality of these objects is the use of metadata columns within the DataWindow to control the visual properties of individual objects rendered on the DataWindow. Within a row on a DataWindow all object properties have access to all data in all columns. This gives you the ability to store visual property data for multiple objects within a single data column. The advantage of row-based storage of graphical object properties is simple, for instance, to display a 60x40 grid of rectangles. If you were to explicitly create each one, you would be generating 2,400 rectangles! Doing this in a DataWindow would create an object of enormous size. Using the row-based technique, simply create 60 rectangles and insert 40 rows of data to get the same 60x40 grid of 2,400 rectangles. You can increase this to a grid of 60x60 rectangles without generating any additional rectangle object by simply adding an additional 20 rows to your DataWindow. The key is storing each of the object properties of the 60 rectangles in a column of data. This is where the concept of DataWindow metadata comes from, with metadata being defined as simply data about data. In this case the data stored in columns is metadata that's parsed into meaningful data used to control DataWindow object visual properties. I use the term graphic objects to refer to any visual DataWindow object including lines, rectangles, ellipses, images, text, and computed objects. Visual object parameters include x, y, width, height, color, line type, pattern, and visibility.
Controlling Visual DataWindow Objects
For illustration purposes let's say we have a DataWindow (dw_1) with one rectangle (r_1). This rectangle represents two states: on and off. On is indicated by the presence of a green rectangle, and off is indicated by no rectangle. The straightforward method for displaying this change of status is to control the visibility of the rectangle by coding:
dw_1.object.r_1.visible = 1 (visible)
dw_1.object.r_1.visible = 0 (not visible)
This method works for one rectangle; however, it will change the visibility property for all r_1 objects on all rows because no row property is used on object properties as is used on data. This then leads to the alternative method: create a numeric column on the DataWindow called r_1_vis. To make this column control the visibility of the rectangle, set the visible property expression of r_1 to r_1_vis. To change the states of r_1 within PowerScript, set the values of r_1_vis[row] = 1 for visible and r_1_vis[row] = 0 for not visible.
The benefit of this method is that you can make row-specific changes to the state of rectangle r_1. Rectangle r_1 can be visible on row 1 and invisible on rows 2 and 3, etc., simply by changing the data in column r_1_vis. You can extend this method by creating columns that control the x, width, y, pattern, and color properties of the rectangle. In doing this, the single rectangle r_1 can have a vastly different appearance for each row. Remember you can have 100 rectangles by creating only one rectangle object on the DataWindow and adding 100 rows of data, and each rectangle can have an appearance completely different from its neighbor.
Applying Metadata to the Visual Properties of DataWindow Objects
As you can see, you can control as many visual parameters as you want by simply adding a column to the DataWindow for each one. Now consider adding a second rectangle, r_2, to the DataWindow. You can then set up additional columns for r_2: r_2.vis, r_2_color, r_2_x, and so on, and use the same method for manipulating the visual properties of rectangle r_2.
What if you wanted up to 20 rectangles on a row? Or what if you wanted to dynamically add and remove rectangles in each row? Creating columns to define the parameters for each graphic object and manipulating them would be difficult and cumbersome considering the number of columns that would need to be changed.
To manipulate five visual properties on 20 rectangles would require 100 columns. If you add to this 20 lines and 20 ellipses, you can see that the number of columns and the amount of code needed to manipulate them becomes enormous. At this point the metadata method of handling all this data makes the task considerably easier.
A more efficient method of storing the visual properties of DataWindow objects is to store each property for all objects in a string DataWindow column. Using this method you can store a property such as visibility for 1 to 1,000 objects in one DataWindow column. The data is actually referenced in the property expression using the DataWindow's built-in LONG and MID functions. For example, if you have 10 rectangles and the task is to control the visibility of each rectangle, create a string(10) column called r_vis. In the visibility expression of rectangle 1 (r_1), put the expression LONG(MID(r_vis,1,1)). This expression places a numeric version of the first character in the metadata column r_vis into the visibility property of rectangle r_1. You would do the same thing for rectangles r_2 through r_10. In the visibility expression of rectangle 2 (r_2), put the expression LONG(MID(r_vis,2,1)); for rectangle 3 (r_3), LONG(MID(r_vis,3,1)). Now, one column, r_vis, controls the visibility for 10 rectangles on one row. The data in one row of column r_vis would look like "0110111010". To completely switch the visibility of the rectangles on this row of data you would simply set r_vis to '1001000101': dw_1.object.r_vis[row_num] = '1001000101'.
To really understand the power of this method, add 100 rows to dw_1. Now you have 1,000 rectangles with visibility properties controlled by one column of data. By using DataWindow metadata to control the graphical properties of a DataWindow object, you can now do graphic manipulations of hundreds of objects by simply doing string manipulations and assignments. You can move the data into the DataWindow directly, or use the faster method of loading arrays and using DataWindow.data expressions to move the data into the DataWindow. For longer properties, such as color defined in eight characters, change the parameters of the MID function to account for the size of the property. To use metadata to control color, create a string column r_color. In the first eight characters, define the color for r_1, in columns 9-16, define the color for r_2, and so on. In the color expression of r_1, put the expression:
In the color expression of r_2, put the expression:
and for r_3:
Using these expressions to make r_1 white, r_2 black, and r_3 red, the column r_color would contain the data "16777215 0 255".
Normally in a graphical presentation of data, the number of graphic items required is unknown just as the number of rows in a report is normally unknown at runtime. This requires that you generate the graphic objects on the DataWindow dynamically. In the previous example, you can create 1 to n number of rectangles at runtime in a loop and generate the offsets required to place the correct values in the property expressions. For instance, in the creation of the minesweep DataWindow, three levels of objects are created to set up the minefield grid (see Figure 3). The first level is a grid of ellipses representing the mines. The second level is the grid of computed fields containing numbers that represent the number of adjacent mines. The third level is a grid of computed fields that cover the cells, and the user can mark a cell by right-clicking.
The visibility of all objects is controlled using metadata and is set up during the creation of the DataWindow objects. The following code shows the building of the mine object layer - note the visibility expression:
In Listing 9 the variable "cnt" is the subscript of the loop and is used to point to the location in the "bomb" string for the visibility property for a particular ellipse object on a particular row.
Now that we have created a metadata DataWindow, the fun starts in manipulating it. In the case of the minesweep application, the manipulation is a simple adjustment of the visibility property of different objects. When a user clicks on a cell, the first thing to do is to make the rectangle that covers the cell not visible. This is done by extracting the rectangle's visibility property Metadata string. This string is contained in a column called "vis". The names of the rectangle objects start with the letter "t" so you must validate that you clicked on a rectangle object and determine which rectangle object on which row you selected. The object name consists of "t'" plus the sequence of the object, for example "t1", "t20", "t45".
The sequence number built into the name determines where in the metadata string to look for that particular rectangle's visibility parameter. For rectangle "t20", look in position 20 of column "vis" to determine the rectangle's visibility. To make the rectangle not visible, just set character 20 in the column "vis" to "0" for the selected row. As seen in Listing 10, the variable "cnt" contains the reference to the location in the visibility string of the visible property for "t20". The visibility expression for "t20" will actually switch the rectangle to not visible.
In some cases you might want to adjust other properties such as color or patterns of an object. The same technique would be used except you'd adjust the position within the column "color" to correctly locate the color property for the object. In the next example, if you were changing the color of the object to red instead of making the object invisible, the following lines of code would change in the construction of the objects. Note that you must account for the larger size, 8 bytes, of the color property. Again, as shown in Listing 11, use the variable "cnt" to determine where in the color string to find the color property for the particular object.
Also make this change in the oe_lbd event to account for the larger size of the color property within the color column. In this case, if the user clicks on the object and you want to change the background color of the object to red, use the following code:
//Extract the visibility metadata
ls_color = this.object.color[row]
// Set to not visible and replace the visibility metadata
this.object.color[row] = REPLACE(ls_color,(ll_seq * 8) - 7,8,'255')
In more sophisticated applications, you'll have a different number of objects on each row on the DataWindow, in this case, the object is a rectangle. The number of rectangles created is determined by which row on the DataWindow requires the largest number of rectangles. For instance, one row might require that 10 rectangles be visible while several rows might not require any. The first thought is that every row must have metadata to control 10 rectangles. However, this is not required, because you only need data for the rectangles actually displayed for the row. In this case, the row with 10 rectangles would have a column controlling visibility called "vis" that contains the following string "1111111111", with each rectangle's visibility being controlled by a single character in the string. On rows with no rectangles visible, you don't need to populate the vis column with all 0s ("0000000000"); rather you can just have an empty string in column vis. Since the expression could not be evaluated, the default value 0 is used in the visible property expression:
and the rectangle is not shown for that row. This behavior makes it easier to maintain a DataWindow with a varied number of objects drawn on each row. To remove an object from a row, determine its sequence number from the name of the object, and then remove the metadata for that object. In this case, you're removing the metadata for the color of rectangle "t2" in the oe_lbd event:
ll_seq = 2
ls_color = ' 016777215 255 0'
// black white red black
dw_planner.object.color[row] = REPLACE(ls_color,(ll_seq * 8) - 7,8,'')
ls_color = ' 016777215 0'
// black white black
Do this same substitution for all properties used, such as width, height, x, y, pattern, and visibility to remove the reference to properties for object "t2". After removing references to the objects properties, make sure that the number of objects rendered equal the number of objects needed. This means that if the maximum number of object required of any row is 9 and you have 10 objects rendered, destroy the highest order object, in this case rectangle "t10" should be destroyed because it doesn't reference any metadata on any row.
To add a new rectangle to a row, simply append the new values to the end of the property value. To add a new green rectangle to a row, do the same extraction of the color value and append the color value for green.
ll_seq = 2
ls_color = ' 016777215 0'
// black white black
dw_planner.object.color[row] = ls_color+' 65280'
ls_color = ' 016777215 0 65280'
// black white black green
Do this same appending of data for all properties used, such as width, height, x, y, pattern, and visibility.
If you add properties for an object that doesn't exist yet, such as in the previous example where there were 10 objects and you add properties for the eleventh, you simply create a new object, in this case rectangle t11 (see Listing 12).
The new object, t11, will now point to the correct position in the metadata for its x, y, width, hatch, color, visibility, and background color.
Assigning Negative Values to Some DataWindow Object Properties
You're allowed to assign negative numbers to property expressions of DataWindow objects. At first this doesn't seem too useful, but when using row-based objects in a graphical format, there might be instances in which you only want to display a portion of a DataWindow object in a row or you might want an object to appear as if it exists between two rows. Figure 4 demonstrates how an object, in this case the yellow rectangles, can appear to exists on more than one row.
Displaying a Row-Based Object Across Multiple Rows
The concept of row-based objects, along with metadata, is used on the appointment object seen in Figure 5 in which appointments are displayed and a single detail band-based object appears to "float" over several rows.
Here the DataWindow object type being manipulated is detail band computed objects that contain the descriptions of the appointments. What is actually happening is that each object exists on every row; however, the y property for each object is different for each row. The y property is calculated in a computed expression so although an object exists on every row, the object appears to exists only once because an expression in the object's y property converts the row-based y property of the object into a y property associated with the DataWindow, not the row. The primary purpose of this technique is to display objects over multiple rows of a DataWindow that are scrollable in the vertical and horizontal directions.
I hope that this chapter has broadened your views about what kind of graphical user interfaces the DataWindow is capable of. Although the DataWindow was designed to present and interact with data primarily as a text-based format, many features exist that can be taken advantage of to create the type of rich graphical interfaces that were presented in this article. There's more to the DataWindow than rows and columns, and by taking advantage of the capabilities of the DataWindow, you can create some interesting interactive graphical interfaces for your programs. Although several business uses of the metadata DataWindow concept were displayed, I hope this article inspires users to come up with many more.
- Where Are RIA Technologies Headed in 2008?
- PowerBuilder History - How Did It Evolve?
- Creation and Consumption of Web Services with PowerBuilder
- Cloud People: A Who's Who of Cloud Computing
- DDDW Tips and Tricks
- Cloud Expo 2011 East To Attract 10,000 Delegates and 200 Exhibitors
- Working with SOA & Web Services in PowerBuilder
- Dynamically Creating DataWindow Objects
- OLE - Extending the Capabilities of PowerBuilder
- DataWindow.NET How To: Data Entry Form