DOORS is already a powerful package, straight from its box. Engineers quickly realize on their first contact with it that it lets them organize, filter, sort, and report on their requirements with a flexibility and ease which is surprisingly comprehensive.
But every project has its own special requirements, which can only be met by going beyond behavior designed for general use. Fortunately, DOORS is accompanied by a built-in extension mechanism, in the form of the DXL engine.
Introducing DXL
DXL is the DOORS eXtension Language. The language itself closely resembles C in its syntax, but with three significant differences.
print square x
As a final example of DXL's lack of clutter, access is simplified by one or two special language constructs that are available in several places. For instance, DXL programmers can simply write
for thisObject in thisModule do …
to access in turn all the objects displayed in a module, without having to know how many objects there might be, or doing complicated things to avoid looking beyond the end of the list of objects.
Custom Graphics with Layout DXL
DXL enables projects to customize almost any desired behavior in their DOORS database – specialized data import and export, custom metrics and reporting, data entry forms, and graphics.
For the sake of example, let us suppose your project wants to capture some of its requirements by using Decision Tree Diagrams to describe business decision-making sequences in your users' area. Decision Trees are simple for both users and engineers to understand, and they also make quite precise what has to happen when.
You can create a decision tree directly using the DOORS user interface to set up a hierarchy of objects containing the decisions and outcomes. If you know how to set up a DOORS Enumeration Type with colors such as Green for Yes and Red for No, then you can display a fully-fledged decision tree using DOORS' built-in graphics mode, like this:
The same data structure could be initialized automatically with DXL, but let us concentrate instead on the graphics.
The default DOORS tree is quite effective; in the example above, the labels in the red and green boxes have been customized by adding (Yes) or (No) as appropriate to show the outcome of the previous decision, as well as the text of the current decision or final outcome. This is achieved by a placing a very few lines of layout DXL 'inside' a DOORS column (The layout DXL editor is accessed by a button in the Column Properties window):
const string oh = "Object Heading" string obo = obj."Binary Outcome" // Yes or No if obo == "Yes" or obo == "No" then display "(" obo ") " obj.oh "" else display obj.oh ""
These retrieve the text in the object heading and a custom Binary Outcome attribute, and display both of them in a column. That column is set to be used for the graphics display, so the effect is to display the combined text in each box of the graphics tree. If you also set the column properties to use Color By Attribute and choose your Binary Outcome attribute, then the graphics display will also be colored as illustrated. Your DOORS module should look something like the one shown below.
If you want your decision tree to allow named outcomes as well as yes or no, then you need to add a third option called "Named" to the Binary Outcome enumeration, and a string attribute called "Named Outcome" to hold the text. The layout DXL code then needs an extra if .. then.. else.. structure to select and display the named outcome if there is one.
Custom Graphics with Regular DXL
However, not everyone is familiar with the appearance of the DOORS object tree, and the business people you talk to may be more used to diagrams consisting of boxes for the questions and arrows to each of the outcomes. They will likely also want to print their diagrams, or export them for inclusion in PowerPoint presentations or other documents. Regular DXL allows you to create dialog boxes with all these features, as the illustration below shows.
Creating the Dialog Box Window
How is it done? The first thing is to create the Dialog Box itself. The DOORS data type for this is called DB:
///////////// Diagram Window ///////////// DB dtbox = create "Decision Tree Diagram"
The next thing is to create a drawing area, known as a canvas, for the diagram. This is an example of a Dialog Box Element, whose data type is DBE. It is declared with an initial width and height, in pixels, and the name of a function whose job it is to repaint the canvas:
void repaint(DBE canv) {} DBE canv = canvas(dtbox, 800, 500, repaint)
We can now immediately test the dialog box by instructing DXL to show it:
///////// launch the main window ///////// show dtbox
If you run this, a dialog box containing an empty canvas and nothing else beyond a Close button is displayed. You can minimize it, maximize it, and close it – in other words, all the tiresome work of setting up the default behavior of a window has been done for you.
Now the real work begins: you need to draw and label the decision boxes, and the arrows between them.
Draw the Arrows
The basis of an arrow is simply a line from A to B, which you can achieve with a single DXL function call:
line(canv, xa, ya, xb, yb )
The arrowhead is a little more complex, and different approaches are possible, such as always drawing arrows directly up/down or left/right, so that only 4 arrowhead symbols are ever needed. However, if we want the simple-looking result, an arrow symbol halfway along each arrow line, then we need to draw the arrowhead in whatever direction is implied by the diagonal between A and B. We can find the midpoint in the x direction just by averaging xa and xb, and similarly for the y direction.
void drawArrowhead(DBE canv, int xa, ya, xb, yb, int &xmid, int &ymid) { const real headSize = 500.0 // relative size of arrowhead xmid = (xb+xa)/2 // midpoint ymid = (yb+ya)/2
This gives the position for the arrowhead; we then need to position the two leading edges of the arrowhead with a tip point a little way along the arrow line, joined to two further points equally spaced either side of the midpoint. We can use Pythagoras' Theorem to find the distance between A and B as a hypotenuse; if this is zero, there is no arrow to draw, and the function can immediately return.
real hypo = sqrt realOf((xb-xa)*(xb-xa) + (yb-ya)*(yb-ya)) // Pythagoras if hypo == 0.0 then return
Otherwise, we set up the tip position by moving along the hypotenuse a proportionate amount:
int pc = 50 + intOf(headSize/hypo) // scale to length of line int x2pos = xa + pc * (xb - xa) / 100 // tip position int y2pos = ya + pc * (yb - ya) / 100
We then find the distance from this point to the midpoint, and draw the two leading edges by connecting points plus/minus this distance to the tip position:
int x = x2pos – xmid // edge distances int y = y2pos - ymid line(canv, xmid + y, ymid - x, x2pos, y2pos ) line(canv, xmid - y, ymid + x, x2pos, y2pos )
If a closed triangle is required for the arrowhead, a third line can be drawn between the edge points.
} // drawArrowhead
Repaint the Canvas
So far we have not drawn anything on the canvas. This is the job of its repaint callback function, so named because it is called whenever the operating system notices that its window has been uncovered or otherwise affected by something else on the display: control is passed back so the window can be redrawn in the way that you specify.
The first thing is to paint the background white or some other suitable color. Then you need to set up the font for drawing box labels; font 9, 2 is the graphics font set up on your DOORS platform for level 9 objects, so it is probably the smallest font in use. To keep the text of each label within its box, we need to choose a sensible box size, and truncate labels that are too big. A simple approach is to size the boxes according to how many levels of decision we have to fit onto the diagram; an alternative would be to use a fixed size label and scroll the diagram as necessary.
void repaint(DBE canv) { background(canv, logicalPageBackgroundColor) font (canv, 9, 2) const string boxtest = "Example Decision Question?" int testboxwidth = width(canv, boxtest) int testboxchars = length boxtest - 2 color(canv, logicalGraphicsBoxEdgeColor) int w = width canv int h = height canv int x = 0 int ymin = 0 int stageheight = h int depth = depthof o // max no of levels below o stagewidth = w/depth boxwidth = 2 * stagewidth / 3 boxchars = testboxchars * boxwidth/testboxwidth // truncate boxheight = 2*yoff + height(canv, "A") // size height to font linkwidth = boxwidth/2 // leave space for arrows
Finally, the repainter has to draw the decision tree on to the canvas, starting with object o.
drawtree(canv, o, x, ymin, stageheight)} // repaint
Find the Depth of the Tree
You may have noticed that this approach calls for a special integer-valued function, depthof, that somehow finds out how many levels there are below a DOORS Object o. This function could be handled in various ways, but an attractively simple solution is to notice that the depth of a leaf Object (namely, one that has no children – computer scientists always mix up tree and family language) is 1, while the depth of any non-leaf Object is its own depth – 1 again – plus the greatest depth of any of its children.
This definition is recursive, as it makes use of itself ('depthof'). This trick works perfectly as long as there is a reliable way of stopping the recursion: otherwise, it goes on forever without returning an answer. In this case, the stopping condition is quite clear: when the Object is a leaf, the returned result is 1. Every tree of finite size must consist of branches that end with leaves, so there is no problem. Here is the algorithm, expressed in DXL. Notice the recursive call to itself, depthof:
int depthof(Object o) { if leaf o then return 1 else { Object child int deepest = 1 int childdepth = 0 for child in o do { childdepth = depthof child // recursive if childdepth > deepest then deepest = childdepth } return 1 + deepest } }
Position the Boxes
Drawing the tree is simply a matter of placing the boxes and their labels on the canvas, and joining them with arrows. Choosing where to place them is, however, quite a delicate matter. The illustration above shows quite a sophisticated algorithm in use; you may like to consider how that tree was drawn. There are several other possible approaches: here is one of the very simplest.
The function, drawtree, is called for a specific Object, 'decision', with a supplied position. The decision box is drawn at that position, and then the available slice of height in the y direction is simply shared out among the children (decision outcomes, in this case – notice how DXL allows the programmer to choose meaningful variable names) of that decision Object. The function concludes in much the same way as the depthof function, calling itself recursively to draw the subtrees for each of its decision outcomes.
This version of drawtree works fine for small decision trees, but runs out of height if there are too many levels. The illustrated version reuses the available height at each level in the decision tree.
////////////////// draw decision tree recursively ////////////// void drawtree(DBE canv, Object decision, int x, int ymin, int decstageheight) { int y = ymin + decstageheight/2 drawbox(canv, decision, x, y) int ci = 0 Object outcome for outcome in decision do ci++ // count outcomes if ci == 0 then return int stageheight = decstageheight/ci // divide height by no of outcomes for outcome in decision do { drawtree (canv, outcome, x+stagewidth, ymin, stageheight) drawarrow(canv, outcome, x+boxwidth, y+boxheight/2, x+stagewidth, ymin+(stageheight+boxheight)/2) ymin = ymin + stageheight } } // drawtree
Enable Print & Export
As a finishing touch, we add buttons to enable users to print and export the canvas. DXL provides a print function which sends a canvas straight to your usual printers, via the same print dialog that you normally see on your operating system. For export, you need to specify which format you want. A convenient format for PC use is the Windows MetaFile or WMF, as this is very compact, can be drawn at any scale, and can easily be incorporated by Word and PowerPoint among other tools.
These buttons work when they are given control. When you gave the DXL command to show the dialog box, you handed control over to the operating system, and the dialog box then just waits on the screen, doing nothing until control is given back to it. The operating system says, hey DOORS, here's a mouse click in your area. DOORS works out that the click is on the Export button, and looks to see which function it has to call when the button is activated, say exportCanvas. It then gives control back to that function, which is therefore known as a callback:
/////////// Apply Callbacks ///////////// DB exportbox = centered "Export Image File" // extra Dialog Box DBE fn = fileName(exportbox, dir "decision_tree.wmf") void exportImage(DB exportbox) { string fName = get fn // user chooses filename export(canv, fName, "WMF") hide exportbox } ok (exportbox, "Export", exportImage) realize exportbox hide exportbox // hide until needed void exportCanvas (DB box) {show exportbox} void printCanvas (DB box) {print(canv, 1.0, 1.0)} apply(dtbox, "Export Diagram", exportCanvas ) // button on main Dialog Box apply(dtbox, "Print Diagram", printCanvas )
That's all you need to do to create powerful custom graphics displays for your DOORS database. Get customizing!
© Ian Alexander, March 2001