Overblog Folge diesem Blog
Edit post Administration Create my blog

Building a treetable-like form (part 2)

5. August 2009 , Geschrieben von Andreas Weiden Veröffentlicht in #Forms

The task

My last post was about "building a treetable-like form", it ended with a functional version which didn't look very sophisticated. Lets change that.

An example

I'll use the same example as in part 1, the EMPLOYEES-table from hr.

The approach

The problem is not to read the hierarchical data, but to build a nice user interface to display the data. The main difference between a tree and a treetable is, that in a treetable you have a "standard" datarow with separate fields for each column, which, if needed, may also be changed in the treetable. If you scroll through the data, the table scrolls with the data. To achieve this, the component to display the tree must be part of the formsblock, so that for each row of the data there is a piece of tree rendered. So the tree-structure has to be splitted into record-based pieces, each of which has informastion about the current node, but also about preceding and following node. But what different types of renderings are possible in a tree?

Each "record" in the hierarchy exists of a number of square blocks, one for each level of the tree-depth the current row is located in. For each of this "squares" there are different possible renderings, depending on the row itself, but also depending on the preceding and following rows. In detail there are 4 possibilities, i colored different in the example.


Red - The rendering in the red square indicates that the current row is the last one at the specific level (the parent of the row has no other childs).
Green - The green square rendering indicates that there is another row at the same level as the current row (the parent of the row has another child).
Yellow/Blue - These rendering doesn't indicate a state for the current row, but for rows at a higher level. For each higher level there is one square, an yellow square means that the preceding row at the specific levels has no following rows at the same level, the blue one indicates that it has.
So, the treelines are build up of 4 different graphical objects, followed by the node-state, icon and label we also had so far.

If we can provide this information to a javabean-item, its quite simple to implement a treelike rendering. We would provide 1-for the red square, 3-for the yellow square, 2 for the green square, 4 for the blue square
In the example, for the different rows, we would have to provide
 

1 Expanded King
32 Collapsed Zlotkey
32 Collapsed Weiss
32 Collapsed Vollman
32 Expanded Russel
342 Collapsed Tuvault
...    


Row 1 is the top-level node, so there is only one level of tree-rendering and this will be 1 for the red-square-style. Row 2 is a row at tree-level 2, so it has two "squares" to be rendered first a 3 for an empty square, followed by a 2 for the green one. This information is provided for every row, so the complete tree gets rendered like a puzzle of square-blocks.

Fetching the data

So, how to get that data in a single query? After some playing around i came up to this query:

SELECT MASTER,
       DETAIL,
       NEXTDETAIL,
       REPLACE(SUBSTR(SYS_CONNECT_BY_PATH(PREV, '#'), 1,LENGTH(SYS_CONNECT_BY_PATH(PREV, '#'))-1),'#', '') ||
       AKT    || '|' ||
       STATUS || '|' ||
       ICON   || '|' ||
       LABEL            NODESTATE,
       EMPLOYEE_ID,
       LAST_NAME,
       FIRST_NAME,
       EMAIL,
       HIRE_DATE
-- -------------------------------------------------------------------------------------
FROM (SELECT MASTER,
             DETAIL,
             NEXTDETAIL,
             CASE WHEN NO_OF_SAME_LEVEL=COUNT_OF_SAME_LEVEL THEN
               '1'
             ELSE
               '2'
             END AKT,
             CASE WHEN DETAIL IN (SELECT COLUMN_VALUE
                                    FROM TABLE(PK_TREETABLE.FK_GET_EXPANDED)
                                 )
             THEN
               'E'
             ELSE
               'C'
             END STATUS,
             CASE WHEN NO_OF_SAME_LEVEL=COUNT_OF_SAME_LEVEL THEN
               '3'
             ELSE
               '4'
             END PREV,
             ICON,
             LABEL,
             EMPLOYEE_ID,
             LAST_NAME,
             FIRST_NAME,
             EMAIL,
             HIRE_DATE
-- -------------------------------------------------------------------------------------
      FROM (SELECT MASTER,
                   DETAIL,
                   LEAD(MASTER,1) OVER (ORDER BY ROWNUM ASC)               NEXTDETAIL,
                   COUNT(*) OVER (PARTITION BY MASTER ORDER BY ROWNUM ASC) NO_OF_SAME_LEVEL,
                   COUNT(*) OVER (PARTITION BY MASTER)                     COUNT_OF_SAME_LEVEL,
                   ICON,
                   LABEL,
                   EMPLOYEE_ID,
                   LAST_NAME,
                   FIRST_NAME,
                   EMAIL,
                   HIRE_DATE
-- -------------------------------------------------------------------------------------
              FROM (SELECT NVL(MANAGER_ID,-1)             MASTER,
                           EMPLOYEE_ID                    DETAIL,
                           'db_user.gif'                  ICON,
                           LAST_NAME || ',' || FIRST_NAME LABEL,
                           EMPLOYEE_ID,
                           LAST_NAME,
                           FIRST_NAME,
                           EMAIL,
                           HIRE_DATE
                      FROM EMPLOYEES
                    START WITH MANAGER_ID IS NULL
                    CONNECT BY PRIOR EMPLOYEE_ID=MANAGER_ID
                    ORDER SIBLINGS BY LAST_NAME
                   )
             WHERE MASTER IN (SELECT COLUMN_VALUE
                                FROM TABLE(PK_TREETABLE.FK_GET_EXPANDED)
                             )
            ORDER BY ROWNUM
           )
       )           
  START WITH MASTER=-1
  CONNECT BY PRIOR DETAIL=MASTER
 ;


 
The query looks quite complicated at first glance, i'll try to explain it a little beginning form the inner query.
The inner query does a hierarchical query on the employees-table and queries the pseudo-columns

 

MASTER ID of the master-record
DETAIL ID of the record
ICON Icon-Name to be rendered in the tree
LABEL additional text next to the icon


in addition to the "real" table-column. These pseudo-columns are used in the outer queries to determine the node-states and "squares" for the tree-rendering-part.
The second query restricts the data to the "expanded" data hold in the pl/sql-table in the db-package and does some analytical functions on the data, which is needed for the determination of the tree-states.

 

NEXTDETAIL is the master of the next record
NO_OF_SAME_LEVEL is the number of rows on the same level so far
COUNT_OF_SAME_LEVEL is the total number of rows on the same level


The third query now takes the data provided and transforms it as required.

The last query does again a hierarchical query over the result and concatenates the needed values for the rendering bean (the SYS_CONNECT_BY_PATH-part).

 

The javabean

The task for the javabean now is quite simple. My simple overrides the standard-textitem, so that the value is always passed correctly to the bean when the record-navigation in forms takes place (if i would took a Bean-Area i had to do theis synchronization by myself). The data the bean gets consists of 3 parts, separated by pipe:

<square-numbers(1,2,3,4)>|<state (C,E)>|<icon>

The bean now renders one of the four squares for each level given in the bean-value, followed by the appropiate node-state-icon (collapsed or expanded), followed by the custom icon. Here the prototype code

package forms.client;
import java.awt.Graphics;
import java.awt.Image;
import java.awt.Rectangle;
import java.awt.Toolkit;
import java.net.URL;
import java.util.StringTokenizer;
import javax.swing.ImageIcon;
import oracle.forms.handler.IHandler;
import oracle.forms.properties.ID;
import oracle.forms.ui.VTextField;

public class TreeStateItem extends VTextField
{
  private ImageIcon m_collapsed=new ImageIcon(this.getClass().getResource("collapsed.png"));
  private ImageIcon m_expanded=new ImageIcon(this.getClass().getResource("expanded.png"));  
  private Image     m_image;
  private String    m_lastImage="";
  private URL       m_codeBase;  
  private String    m_status;
  private String    m_nodestates;
  private String    m_label;
  private String    m_value="";

 
  public void init(IHandler handler)
  {
    // Remember Codebase
    m_codeBase = handler.getCodeBase();
    super.init(handler);
  }
 
  public void paint(Graphics g)
  {
    Rectangle r=this.getBounds();
    g.setColor(getBackground());
    g.fillRect(0, 0, (int)r.getWidth(), (int)r.getHeight());
    g.setColor(getForeground());
    // Draw lines for nodestates
    if (m_nodestates!=null)
    {
      char[] c=m_nodestates.toCharArray();
      for (int i=0;i<c.length;i++)
      {
        switch (c[i])
        {
          case '1' : g.drawLine(i*16+8, 0, i*16+8, (int)(r.getHeight()/2));
                     g.drawLine(i*16+8, (int)(r.getHeight()/2), i*16+15, (int)(r.getHeight()/2));
                     break;
          case '2' : g.drawLine(i*16+8, 0, i*16+8, (int)r.getHeight());
                     g.drawLine(i*16+8, (int)(r.getHeight()/2), i*16+15, (int)(r.getHeight()/2));
                     break;
          case '3' : break;
          case '4' : g.drawLine(i*16+8, 0, i*16+8, (int)r.getHeight());
                     break;
        }
      }
      if ("E".equals(m_status))
      {
        g.drawImage(m_expanded.getImage(), (c.length)*16+1, 3, 16, 16, m_expanded.getImageObserver());
      } else if ("C".equals(m_status))
      {
        g.drawImage(m_collapsed.getImage(), (c.length)*16+1, 3, 16, 16, m_collapsed.getImageObserver());
      }
      if (m_image!=null)
      {
        g.drawImage(m_image, (c.length+1)*16+1, 1, 16, 16, null);
      }
    }
  }
 
  private void loadImage(String imageName)
  {
    // LoadImage, taken from oracle-demo RolloverButton
    URL imageURL = null;
    boolean loadSuccess = false;
    if (!(imageName==null || "".equals(imageName)))
    {
      if (!imageName.equals(m_lastImage))
      {
        //JAR
        imageURL = getClass().getResource("/"+imageName);
        if (imageURL != null)
        {
          try
          {
            m_image = Toolkit.getDefaultToolkit().getImage(imageURL);
            loadSuccess = true;
          }
          catch (Exception ilex)
          {
          }
        }
        //DOCBASE
        if (!loadSuccess)
        {
          try
          {
            if (imageName.toLowerCase().startsWith("http://")||imageName.toLowerCase().startsWith("https://"))
            {
              imageURL = new URL(imageName);
            }
            else
            {
              imageURL = new URL(m_codeBase.getProtocol() + "://" + m_codeBase.getHost() + ":" + m_codeBase.getPort() + imageName);
            }
            try
            {
              m_image = createImage((java.awt.image.ImageProducer) imageURL.getContent());
              loadSuccess = true;
            }
            catch (Exception ilex)
            {
            }
          }
          catch (java.net.MalformedURLException urlex)
          {
          }
        }
        //CODEBASE
        if (!loadSuccess)
        {
          try
          {
            imageURL = new URL(m_codeBase, imageName);
            try
            {
              m_image = createImage((java.awt.image.ImageProducer) imageURL.getContent());
              loadSuccess = true;
            }
            catch (Exception ilex)
            {
            }
          }
          catch (java.net.MalformedURLException urlex)
          {
          }
        }
        // remember last image
        m_lastImage=imageName;
      }
    }
  }
 
  public boolean setProperty(ID property, Object value)
  {
    if (property==ID.VALUE)
    {
      // "Catch" the value, tokenize it and store it in member vars
      m_value=(String)value;
      StringTokenizer st=new StringTokenizer(m_value, "|");
      m_nodestates="";
      m_status="";
      m_label="";
      if (st.hasMoreTokens())
      {
        m_nodestates=st.nextToken();
      }
      if (st.hasMoreTokens())
      {
        m_status=st.nextToken();
      }
      String imagename=null;
      if (st.hasMoreTokens())
      {
        imagename=st.nextToken();
        loadImage(imagename);
      }
      if (st.hasMoreTokens())
      {
        m_label=st.nextToken();
      }
      if (imagename==null)
      {
        m_image=null;
        m_lastImage=null;
      }
      // pass an empty value to the superclass
      return super.setProperty(property, "");
    } else
    {
      return super.setProperty(property, value);
    }
  }

  public Object getProperty(ID property)
  {
    // TODO:  Override this oracle.forms.ui.VTextField method
    if (property==ID.VALUE)
    {
      // return the locally stored value
      return m_value;
    } else
    {
      return super.getProperty(property);
    }
  }
}

 

 

The forms-part

Putting everything together, there is not much logic in the form itself. I created a view based on the formerly discussed query. Next step is to create a tabular block based on that view.
Then, add the triggers as discussed in part 1 of the article. Last, set the implementation class of the item containing the node-state to forms.client.TreeStateItem. And finally, as for every java-bean, wrap the javabean in a jar and put it into the archive or archine_jini-tag in the formsweb.cfg.
 

 

 

 

Diesen Post teilen

Repost 0

Kommentiere diesen Post