Eve Application Development

Michael L Brereton - 02 February 2008, http://www.ewesoft.com/

 

Beginners Guide - Contents

<< Previous: Images and Pictures

>> Next: Table Controls

 

Drawing to Controls

Eve Application Development

Drawing to Controls

Example Custom Control

 

You will want to draw on controls either:

 

  1. In response to a repaint request by the platform.
  2. Directly to reflect changes in the control state or for animation.

 

The main method calls to request a repaint of a control are the repaintNow() or repaintNow(Graphics g, Rect area) methods. These result in a call to doPaint(Graphics g, Rect area) for that control and all its child controls. This is a synchronous call – the doPaint() method is called in the same thread as the call to repaintNow(). To request a repaint that occurs within the Window event thread you should call repaint() or repaint(int x, int y, int width, int height) instead. You do not need to supply a Graphics for repaintNow(), one will be created if the Graphics parameter is null.

 

When parts of the Window are invalidated the Window calls a repaintNow() in the event thread for the area that needs repainting which then results in a call to doPaint().

 

Custom drawing of a Control will therefore require you to override the doPaint(Graphics g, Rect area) method. The supplied area parameter is the area within the Control that needs repainting. The Graphics provided will be a fully buffered Graphics object, i.e. it draws to an off-screen Image which is then drawn on the screen when all drawing is done.

 

If you need to draw directly to an area on the Control when not in the doPaint() method you should call:

 

BufferedGraphics getGraphics(int x, int y, int width, int height);

 

If the Control is not visible on the screen (it may not be in a visible Window) then this returns null. Otherwise the method returns a BufferedGraphics object from which you get a Graphics object by calling getGraphics(). You draw on the returned Graphics object (you must draw on the entire requested area) and then call release() on the BufferedGraphics when complete. This will update the area on the Control with what you have drawn on the Graphics.

 

Note that the Graphics returned by BufferedGraphics.getGraphics()  has a coordinates relative to the Control, and not relative to the x and y parameters specified in Control.getGraphics(). For example the calls:

 

 
BufferedGraphics bg = getGraphics(10,20,100,50);
if (bg == null) return;
Graphics g = bg.getGraphics();
g.drawLine(10,20,50,50);
//Do other drawing.
bg.release();
 

 

The g.drawLine(10,20,50,50); will start at (10,20) within the control. Even though you have only requested to draw in a section of the control, the Graphics will still be relative to the entire control – however any drawing outside of the requested area will not affect the on-screen control.

 

Remember to draw the background for the control as well. By calling getGraphics() you invalidate the requested area and so must repaint all data within it.

Example Custom Control

Here is an example of a simple custom control. It will consist of a grid of cells with some simple text within each cell. Initially, each cell will be invisible until the user presses the pen/mouse over that cell. First we’ll do the constructor and then an example main() method to display the control within a Form.

 

package evesamples.ui;
 
import eve.fx.BufferedGraphics;
import eve.fx.Color;
import eve.fx.FontMetrics;
import eve.fx.Graphics;
import eve.fx.Rect;
import eve.ui.Application;
import eve.ui.CellPanel;
import eve.ui.Control;
import eve.ui.Form;
import eve.ui.event.PenEvent;
 
public class GridControl extends Control{
 
     int numRows, numCols, cellWidth, cellHeight;
     boolean[] visible;
     
     public GridControl(int rows, int cols)
     {
             numRows = rows;
             numCols = cols;
             visible = new boolean[rows*cols];
             backGround = Color.White;
     }
     public static void main(String args[])
     {
             Application.startApplication(args);
             Form f = new Form();
             f.title = "Test GridControl";
             CellPanel cp = new CellPanel();
             cp.setText("GridControl");
             cp.addLast(new GridControl(4,4));
             f.addLast(cp);
             f.doButtons(Form.OKB);
             f.execute();
             Application.exit(0);
     }
}

 

Now we would like for the control to be able to report a preferred size – but we would like it to calculate the size based on the number of rows and columns in the grid. To do that we override calculateSizes() and within that method we set the fields preferredWidth and preferredHeight. This method is usually only called once (unless it is forced by a relayout() call) and when the method is called you can call getFont() or getFontMetrics() to determine the font that has been assigned to it.

 

     /**
      * This method is called (usually only once) and is used
      * by the Control to calculate its preferred,minimum and maximum sizes.
      */
     protected void calculateSizes()
     {
             FontMetrics f = getFontMetrics();
             cellWidth = f.getTextWidth("(00,00)")+4;
             cellHeight = f.getHeight()+4;
             preferredWidth = cellWidth*numCols;
             preferredHeight = cellHeight*numCols;
     }
     /**
      Use this to determine the area of a particular cell in the control.
     **/
     public Rect getCellRect(int row, int col, Rect dest)
     {
             if (dest == null) dest = new Rect();
             dest.x = col*cellWidth;
             dest.y = row*cellHeight;
             dest.width = cellWidth;
             dest.height = cellHeight;
             return dest;
     }
 

 

Now we use this method to paint the control.

 

     public void doPaint(Graphics g, Rect area)
     {
             for (int r = 0; r<numRows; r++)
                     for (int c = 0; c<numCols; c++)
                            paintCell(r,c,g);
     }

 

The doPaint() method is called in response to a repaint() or repaintNow() call which can be called explicitly or is called when the window needs refreshing because part of it has been covered and then exposed. The method paintCell() has not been defined yet so we need to define it now:

 

     private void paintCell(int row, int col, Graphics g)
     {
             boolean alwaysPaint = true;
             Rect r = Rect.getCached();
             try{
                     getCellRect(row, col, r);
                     //
                     BufferedGraphics bg = null;
                     if (g == null){
                            bg = getGraphics(r.x, r.y, r.width, r.height);
                            if (bg == null) return;
                            g = bg.getGraphics();
                     }
                     //
                     g.setColor(getBackground());
                     g.fillRect(r.x, r.y, r.width, r.height);
                     //
                     if (alwaysPaint || visible[row*numCols+col]){
                            g.setColor(getForeground());
                            g.drawRect(r.x,r.y,r.width,r.height);
                            String myLabel = "("+row+","+col+")";
                            int w = getFontMetrics().getTextWidth(myLabel);
                            g.setFont(getFont());
                            g.drawText(myLabel,r.x+((cellWidth-w)/2),r.y);
                     }
                     if (bg != null) bg.release();
             }finally{
                     r.cache();
             }
     }
 

 

The first line defines a Boolean variable alwaysPaint and sets it true. We use this initially to display all the cells regardless of the state of the visible[] flag for the cell. As long as alwaysPaint is true, then all cells will always be painted.

 

Note the line Rect r = Rect.getCached(); This gets a Rect object for temporary use that will later be released via a cache() call. Objects can be fetched out of the cache very quickly (quicker than allocating a new one) and as long as it is replaced back in the cache it will cause no object to be garbage collected. Any object can be cached using the eve.sys.Cache class but commonly used objects like Rect and Point have their own methods to do this for convenience.

 

Using this Rect we get the area for the cell we are painting with the getCellRect() method and now we have to paint within that area. Now when the method is called from doPaint() the Graphics object g will already be a valid Graphics, but there will be another circumstance when we call this method without a valid Graphics object. In that case the parameter g will be null and we will have to create our Graphics for the Control.

                     BufferedGraphics bg = null;
                     if (g == null){
                            bg = getGraphics(r.x, r.y, r.width, r.height);
                            if (bg == null) return;
                            g = bg.getGraphics();
                     }

 

That is the purpose of the call to getGraphics(). It creates and returns a (cached) BufferedGraphics object for the specified area. We first make sure it is not null (which would indicate that the control is not actually on screen) and then we get a Graphics object by calling getGraphics() on the BufferedGraphics.

 

Now we can draw the cell using the Graphics g which we know will now be valid. First we make sure to paint the background of the Control.

 

                     g.setColor(getBackground());
                     g.fillRect(r.x, r.y, r.width, r.height);

 

Then we paint the inside of each cell. Later we will set alwaysPaint to false.

 

                     if (alwaysPaint || visible[row*numCols+col]){
                            g.setColor(getForeground());
                            g.drawRect(r.x,r.y,r.width,r.height);
                            String myLabel = "("+row+","+col+")";
                            int w = getFontMetrics().getTextWidth(myLabel);
                            g.setFont(getFont());
                            g.drawText(myLabel,r.x+((cellWidth-w)/2),r.y);
                     }
                     if (bg != null) bg.release();

 

When we run this we get this window:

 

 

Now we’ll set alwaysPaint to false and when we run it we will see only a blank white rectangle. So now we will add code to react to a pen/mouse press. Pressing the mouse on a particular cell will make it visible or invisible.

 

     public void onPenEvent(PenEvent pen)
     {
             if (pen.type == PenEvent.PEN_DOWN){
                     int r = pen.y/cellHeight;
                     int c = pen.x/cellWidth;
                     if (r >= 0 && r <numRows && c >= 0 && c <numCols){
                            visible[r*numCols+c] = !visible[r*numCols+c];
                            paintCell(r, c, null);
                     }
             }
             super.onPenEvent(pen);
     }

 

The onPenEvent() is called by the default onEvent() method of Control. Here we are reacting to PEN_DOWN but we could also react to PEN_UP in this method. Note that normally, for performance reasons, a Control will not receive PEN_MOVED events. We will show how to receive and react to these events a little later.

 

Now when we run the application we get a blank rectangle again, but pressing the mouse on the control will reveal and hide the cells.

 

 

Now we will modify the program to handle the mouse cursor moving over the control by displaying the cell under the mouse in a light blue color. First we will add a field to hold the cell the mouse was last over. This is used so that we don’t continuously repaint the same cell if the mouse is moving within a single cell. Then we will request that the control receive PEN_MOVE events in the constructor.

 

     Point mouseOver;
     
     public GridControl(int rows, int cols)
     {
             numRows = rows;
             numCols = cols;
             visible = new boolean[rows*cols];
             backGround = Color.White;
             PenEvent.wantPenMoved(this, PenEvent.WANT_PEN_MOVED_ONOFF|PenEvent.WANT_PEN_MOVED_INSIDE, true);
     }

 

Now the control will receive PEN_MOVE and PEN_MOVED_OFF events. We will alter the paintCell() method to paint the cell that the mouse is over differently.

 

                     boolean over = (mouseOver != null && mouseOver.x == col && mouseOver.y == row); 
                     if (over || alwaysPaint || visible[row*numCols+col]){
                            g.setColor(over ? Color.LightBlue : getForeground());
                            g.drawRect(r.x,r.y,r.width,r.height);
                            String myLabel = "("+row+","+col+")";
                            int w = getFontMetrics().getTextWidth(myLabel);
                            g.setFont(getFont());
                            g.drawText(myLabel,r.x+((cellWidth-w)/2),r.y);
                     }

 

And lastly we will modify the onPenEvent() method to handle the PEN_MOVE and PEN_MOVED_OFF events.

 

     public void onPenEvent(PenEvent pen)
     {
             int oldx = -1, oldy = -1;
             if (mouseOver != null){
                     oldx = mouseOver.x;
                     oldy = mouseOver.y;
             }
             if (pen.type == PenEvent.PEN_MOVED_OFF && mouseOver != null){
                     mouseOver = null;
                     paintCell(oldy,oldx,null);
             }
             int r = pen.y/cellHeight;
             int c = pen.x/cellWidth;
             if (r < 0 || r >= numRows || c < 0 || c >= numCols) {
                     super.onPenEvent(pen);
                     return;
             }
             if (pen.type == PenEvent.PEN_DOWN){
                     visible[r*numCols+c] = !visible[r*numCols+c];
                     paintCell(r, c, null);
             }else if (pen.type == PenEvent.PEN_MOVE){
                     if (oldx != c || oldy != r){
                            if (mouseOver != null){
                                    mouseOver = null;
                                    paintCell(oldy,oldx,null);
                            }
                            mouseOver = new Point(c,r);
                            paintCell(r,c,null);
                     }
             }
             super.onPenEvent(pen);
     }