Recall that the device display displays the current displayable, which can be a screen or a canvas:
To do graphics programming, we need to set the current displayable to a custom canvas:
class GraphicsDemo extends MIDlet {
private Display theDisplay =
Display.getDisplay(this);
private Canvas canvas = new
DemoCanvas();
public void startApp() {
theDisplay.setCurrent(canvas);
}
// etc.
}
A custom canvas implements the paint() method:
class DemoCanvas extends Canvas {
public void paint(Graphics g) {
// paint on g
}
// etc.
}
A standard example is a paint program that allows the user to move a cursor around the screen with the pointer control. In the "Pen Up" state, no line is drawn as the cursor moves. In the "Pen Down" state, a line is drawn:
The Paint midlet constructor creates a Paint canvas that fires three commands: "Pen Up", "Pen Down", and "Erase":
public class Paint extends MIDlet
implements CommandListener {
// Device display window &
displayble:
private Display theDisplay;
private PaintCanvas canvas;
// pre-define commands:
private final static Command CMD_EXIT
= new Command("Exit",
Command.EXIT, 1);
private final static Command CMD_ERASE
= new Command("Erase",
Command.SCREEN, 1);
private final static Command CMD_PENUP
= new Command("Pen Up",
Command.SCREEN, 1);
private final static Command
CMD_PENDOWN
= new Command("Pen Down",
Command.SCREEN, 1);
// create canvas:
public Paint() {
theDisplay = Display.getDisplay(this);
canvas = new PaintCanvas();
canvas.addCommand(CMD_EXIT);
canvas.addCommand(CMD_ERASE);
canvas.addCommand(CMD_PENUP);
canvas.addCommand(CMD_PENDOWN);
canvas.setCommandListener(this);
}
// Lifecycle methods:
protected void destroyApp(boolean
unconditional) { }
protected void pauseApp() { }
protected void startApp() {
theDisplay.setCurrent(canvas);
}
// Command handler method:
public void commandAction(Command c,
Displayable d) { ... }
}
The midlet handles commands fired by the canvas by setting flags or calling canvas methods:
void commandAction(Command c, Displayable d) {
switch(c.getCommandType()) {
case Command.SCREEN:
if (c == CMD_PENUP) canvas.penUp
= true;
else if (c == CMD_PENDOWN)
canvas.penUp = false;
else if (c == CMD_ERASE)
canvas.erase();
break;
case Command.EXIT:
destroyApp(false);
notifyDestroyed();
} // switch
} // commandAction()
Every Canvas subclass must implement the abstract paint() method. This method is automatically called when the device decides the screen needs to be refreshed, or when the application calls one of the variants of the repaint() method.
The paint() method is passed a new graphical context object of type Graphics. This object provides methods for drawing shapes, lines, and text.
The canvas is provided with a coordinate system. The origin is the upper left corner, the x-axis runs across the top of the canvas, the y-axis runs backwards down the left edge of the canvas. The unit of measurement is the PIXEL.
To make things easier, we declare a point class. When a point renders itself, it draws a solid 5 x 5 rectangle:
class Point {
public int xc = 0, yc = 0;
public static final int DIAM = 5;
public Point(int x, int y) {
xc = x;
yc = y;
}
public Point(Point p) {
xc = p.xc;
yc = p.yc;
}
public void paint(Graphics g) {
g.fillRect(xc, yc, DIAM, DIAM);
}
}
Our PaintCanvas maintains a cursor point and a vector of points contained in the drawing:
class PaintCanvas extends Canvas {
public static final int DELTA = 5; //
cursor speed
public Point cursor = new Point(5,
5);
public boolean penUp = false;
public Vector points = new Vector();
public void erase() {
points.removeAllElements();
repaint();
}
public void paint(Graphics g) { ... }
public void keyPressed(int keyCode) {
... }
}
The paint() method must clear the canvas by drawing a large white solid rectangle. After the cursor is drawn, the points vector is traversed and each member is drawn:
void paint(Graphics g) {
// Clear background:
g.setColor(0x00ffffff);
g.fillRect(0, 0, getWidth(),
getHeight());
g.setColor(255, 0, 0);
// plot cursor:
cursor.paint(g);
// plot drawn points:
for(int i = 0; i < points.size();
i++) {
Point next = (Point)points.elementAt(i);
next.paint(g);
}
}
Canvases may receive low-level events from the device. When a key is pressed, the keyPressed() method is automatically called. This method moves the cursor slightly, adds the cursor position to the vector if the pen state is down, then asks the device to redraw the graphics:
void keyPressed(int keyCode) {
switch (getGameAction(keyCode)) {
case Canvas.UP:
cursor.yc
= (cursor.yc < 0)?
getHeight():(cursor.yc - DELTA);
break;
case Canvas.DOWN:
cursor.yc = (cursor.yc + DELTA) %
getHeight();
break;
case Canvas.LEFT:
cursor.xc
= (cursor.xc < 0)?
getWidth(): (cursor.xc - DELTA);
break;
case Canvas.RIGHT:
cursor.xc = (cursor.xc + DELTA) %
getWidth();
break;
case 0:
// no game action, use num pad
instead
}
if (!penUp) points.addElement(new
Point(cursor));
repaint();
}
MIDP also requires conforming devices to provide support for multi-threading. This is useful for game programming, where sprites are moved by threads while the main thread listens for user inputs.
In this example, a new bouncing "ball" is created each time the user presses 1. Pressing the "Kill" soft button kills all of the balls. Each ball is moved by its own thread of control:
The Bounce application is a slight variation of the Paint application. The Bounce midlet uses a Bounce canvas instead of a Paint canvas. The bounce canvas only fires a KILL command, which is handled by the midlet by calling the erase() method of the canvas:
public class Bounce extends MIDlet
implements CommandListener {
// Device display window &
displaybles:
private Display theDisplay;
private BounceCanvas canvas;
// pre-define commands:
private final static Command CMD_EXIT
= new Command("Exit",
Command.EXIT, 1);
private final static Command CMD_KILL
= new Command("Kill",
Command.SCREEN, 1);
public Bounce() {
theDisplay =
Display.getDisplay(this);
canvas = new BounceCanvas();
canvas.addCommand(CMD_EXIT);
canvas.addCommand(CMD_KILL);
canvas.setCommandListener(this);
}
// Lifecycle methods:
protected void destroyApp(boolean
unconditional) {
canvas.erase();
}
protected void pauseApp() { }
protected void startApp() {
theDisplay.setCurrent(canvas);
}
// Command handler method:
public void commandAction(Command c,
Displayable d) {
if (c == CMD_KILL) {
canvas.erase();
} else if (c == CMD_EXIT) {
destroyApp(false);
notifyDestroyed();
}
} // commandAction()
}
The Bounce canvas is similar to the Paint canvas. Instead of a vector of points, it contains a vector of balls:
class BounceCanvas extends Canvas {
private Random numberGen = new
Random();
public Vector balls = new Vector();
// Ball is an inner class:
class Ball extends Thread { ... }
public void erase() { ... }
public void paint(Graphics g) { ... }
public void keyPressed(int keyCode) {
... }
}
Before emptying the vector and repainting, the erase() method stops each ball:
void erase() {
for(int i = 0; i < balls.size();
i++) {
Ball b = (Ball)balls.elementAt(i);
b.stop();
}
balls.removeAllElements();
repaint();
}
The paint() method asks each ball to paint itself:
void paint(Graphics g) {
// Clear background:
g.setColor(0x00ffffff);
g.fillRect(0, 0, getWidth(),
getHeight());
// g.setColor(255, 0, 0);
// plot drawn balls
for(int i = 0; i < balls.size();
i++) {
Ball next =
(Ball)balls.elementAt(i);
next.paint(g);
}
}
When the user presses 1, the keyPressed() method creates a new ball and adds it to the vector:
void keyPressed(int keyCode) {
if (keyCode == Canvas.KEY_NUM1) {
Ball b = new Ball();
balls.addElement(b);
b.start();
}
}
A ball is like a point derived from Thread. It is declared as an inner class so that it can access the methods and fields of the outer Canvas class. A random number generator is used to compute the initial position of the ball:
class Ball extends Thread {
// current & former positions:
private int xc, yc, oldxc, oldyc;
// x & y speeds:
private int dx = 3, dy = 3;
// halting machinery:
private boolean halt = false;
public void stop() { halt = true; }
public Ball() {
xc = Math.abs(numberGen.nextInt()) %
getWidth();
yc = Math.abs(numberGen.nextInt()) %
getHeight();
}
private void move() { ... }
public void run() { ... }
public void paint(Graphics g) { ... }
}
The move() method simply updates the ball's position:
void move() {
oldxc = xc;
oldyc = yc;
xc += dx;
yc += dy;
if (xc <= 0 || getWidth() <= xc)
dx = -dx;
if (yc <= 0 || getHeight() <= yc)
dy = -dy;
}
The run() method perpetually calls the move() method, then repaints the portion of the canvas containing the former and current positions of the ball:
void run() {
while(!halt) {
move();
repaint(oldxc - 5, oldyc - 5, 15,
15);
// sleep for 10 msecs to be cooperative:
try { Thread.sleep(10); }
catch(InterruptedException e) {}
}
}
The paint() method simply draws a blue solid rectangle:
void paint(Graphics g) {
int oldColor = g.getColor();
g.setColor(0, 0, 255);
g.fillRect(xc, yc, 5, 5);
g.setColor(oldColor);
}
MIDP/CLDC platforms can also run client software for network applications. For example, a web server might service J2ME and WAP mini-browsers as well as desk-top browsers.
J2ME declares a new connection framework:
Our client simply specifies a URL and a request. The server echos the request back to the client:
A client-side server proxy decouples the application from the technology used to send messages across process boundaries. The main method-- postRequest()-- writes a string into a data output stream that comes from an HTTP connection. The response is read using the getData() method:
class ServerProxy {
private String sessionID;
private HttpConnection conn;
public String postRequest(String url,
String data) {
String response = new String();
try {
conn = makeConnection(url);
conn.setRequestMethod(HttpConnection.POST);
DataOutputStream dos =
conn.openDataOutputStream();
dos.writeUTF(data);
dos.flush();
response = getData(conn);
conn.close();
} catch(IOException ioe) {
System.err.println("--->
" + ioe);
if (conn != null) conn.close();
}
return response;
} // postRequest
// helpermethods:
private HttpConnection
makeConnection(String url) { ... }
private void
setUserAgentHeader(HttpConnection conn) { ... }
private String getData(HttpConnection
conn) { ... }
} // ServerProxy
The server will need to know the type of client. This information is determined from the HTTP header, which mentions that the client is a MIDP device:
private void setUserAgentHeader(HttpConnection conn)
throws IOException {
String header = "Profile=" ;
header +=
System.getProperty("microedition.profiles");
header += " Configuration=";
header +=
System.getProperty("microedition.configuration");
conn.setRequestProperty("User-Agent",
header);
}
The getData() method reads from a data input stream associated with the connection. The data is read on character at a time until a sentinel value of -1 signals the end of the message:
private String getData(HttpConnection conn)
throws IOException {
String result = "";
DataInputStream dis =
new DataInputStream(
conn.openInputStream());
int next;
do {
next = dis.read();
result += (char)next;
} while(next != -1);
dis.close();
return result;
}
The midlet uses a form to gather the request and URL. A text box will be used to display the server's response:
public class Client extends MIDlet
implements CommandListener {
// soft button commands:
private final static Command CMD_EXIT
= new Command("Exit",
Command.EXIT, 1);
private final static Command CMD_OK
= new Command("OK",
Command.OK, 1);
private final static Command CMD_BACK
=
new Command("Back", Command.BACK, 1);
// Device display window, screens,
& fields:
private Display theDisplay;
private Form mainScreen;
private TextField urlField, reqField;
private TextBox responseScreen;
// server connection:
private ServerProxy server;
public Client() { ... }
// Lifecycle methods:
protected void destroyApp(boolean
unconditional) { }
protected void pauseApp() { }
protected void startApp() {
theDisplay.setCurrent(mainScreen);
}
// Command handler:
public void commandAction(Command c,
Displayable d) { ... }
} // Client
public Client() {
theDisplay =
Display.getDisplay(this);
mainScreen = new Form("Mini
Browser");
urlField = new TextField("URL:
", "", 50, TextField.URL);
reqField = new TextField("REQ:
", "", 50, TextField.ANY);
mainScreen.append(urlField);
mainScreen.append(reqField);
server = new ServerProxy();
responseScreen
= new TextBox("server
response", "???", 50, TextField.ANY);
responseScreen.addCommand(CMD_BACK);
responseScreen.setCommandListener(this);
mainScreen.addCommand(CMD_OK);
mainScreen.addCommand(CMD_EXIT);
mainScreen.setCommandListener(this);
}
The command handler reads the information in the text fields, then uses the server proxy to connect with the real server:
public void
commandAction(Command c, Displayable d) {
switch(c.getCommandType()) {
case Command.BACK:
theDisplay.setCurrent(mainScreen);
break;
case Command.OK:
String url =
urlField.getString();
String req =
reqField.getString();
String resp =
server.postRequest(url, req);
responseScreen.setString(resp);
theDisplay.setCurrent(responseScreen);
break;
case Command.EXIT:
destroyApp(false);
notifyDestroyed();
break;
} // switch
} // commandAction()
}
The echo server is implemented as a pair of servlets. A J2EE compliant web server (such as Tomcat) will pass the client's request to the indicated servlet. The servlet sends an HTML or text response back to the client.
We employ the Model-View-Controller pattern on the server-side, too. The role of the controller is played by a dispatcher servlet, which determines which view should be displayed. In our servlet the determination of the view depends on the type of client: IE browser, WAP client, J2ME client, I-Mode client, etc:
public class Dispatcher extends HttpServlet {
// client types:
public static final String IE_HEADER =
"MSIE 5";
public static final String WAP_HEADER =
"UP";
public static final String IMODE_HEADER
= "Pixo";
public static final String J2ME_HEADER
= "MIDP";
protected void doPost( ... ) { ... }
protected void dispatch( ... ) { ... }
}
The doPost() method is automatically called by the server when HTTP POST requests are received:
void doPost(
HttpServletRequest request,
HttpServletResponse response)
throws ServletException, IOException
{
String viewPath =
"/unknownView";
try {
String userAgent =
request.getHeader("User-Agent");
if (userAgent.indexOf(IE_HEADER)
!= -1)
viewPath =
"/IEView";
else if
(userAgent.indexOf(WAP_HEADER) != -1)
viewPath =
"/WAPView";
else if
(userAgent.indexOf(IMODE_HEADER) != -1)
viewPath =
"/ImodeView";
else if
(userAgent.indexOf(J2ME_HEADER) != -1)
viewPath = "/J2meView";
else
viewPath =
"/IEView";
}
catch(Exception e) { }
dispatch(request, response,
viewPath);
} // processRequest()
The actual dispatch is done by a special method that asks the server to redirect the request to a particular view:
void dispatch(
HttpServletRequest request,
HttpServletResponse response,
String page)
throws ServletException,
java.io.IOException {
ServletContext container =
getServletContext();
RequestDispatcher dispatcher =
container.getRequestDispatcher(page);
dispatcher.forward(request,
response);
}
As its name indicates, a view servlet plays the role of a view. It might fetch dynamic content from the model, which might be played by a DAO object connected to a data base using JDBC. In our case there is no dynamic content other than the request itself:
public class J2meView extends HttpServlet {
protected void doPost(
HttpServletRequest request,
HttpServletResponse response)
throws ServletException, IOException
{
try {
response.setContentType("text/plain");
PrintWriter out =
response.getWriter();
out.println("Hello J2ME, you
sent: ");
BufferedReader in =
request.getReader();
String msg =
in.readLine().trim();
out.println(msg);
}
catch(Exception e) { }
}
}