메뉴 바로가기 검색 및 카테고리 바로가기 본문 바로가기

한빛출판네트워크

IT/모바일

J2ME의 CLDC/MIDP MIDlet 프로그래밍(1)

한빛미디어

|

2001-06-12

|

by HANBIT

10,227

by 한빛리포터 1기 신명수 J2ME(Java 2 MicroEdition)는 임베디드 디바이스의 다양하고 제한된 환경에서 자바 환경을 탑재하기 위한 플랫폼이다. J2ME는 다른 자바 플랫폼과 달리 컨피겨레이션(Configuation)과 프로파일(profile)로 세분화 하는데 이것은 다양한 디바이스 사이에서 이식성을 최대한 확보하기 위한 것이라 할 수 있다. J2ME을 두 개로 나눈 컨피겨레이션(configuration)은 CLDC(Connected,Limited Device Configuration)과 CDC(Connected Device Configuration)가 있다. 디바이스의 성능으로 구분지었다고 할 수 있는데, CLDC(Connected Limited Device Configuration)은 기본 가상머신으로 기존의 가상머신 대신 작은 용량을 차지하는 KVM과 모바일, 퍼스널, 네트워크 디바이스에 사용되는 J2SE의 코어 API의 서브셋을 포함한 컨피겨레이션이다. 자원이 작아 낮은 사양의 PDA, 삐삐, 휴대 전화 등의 디바이스에 이용된다. 이에 반해, CDC는 32비트 CPU와 충분한 메모리를 가진 디바이스군을 지원하는 컨피겨레이션이다. 대표적인 디바이스로 셋톱박스, 카 네비케이션 등이 있다. 이런 컨피겨레이션 위에 프로파일(Profile)이 탑재되는데, 이는 컨피겨레이션으로 묶여진 디바이스들을 더욱 세분화해서 지원하는 API군이라 할 수 있다. 대표적인 것으로 MIDP(Mobile Information Device Profile)이 있다. MIDP는 CLDC위의 모바일 디바이스를 지원하는 프로파일이다. 즉 CLDC에서 지원하지 않는 유저 인터페이스, 애플리케이션 생명주기(Application Lifecycle), 영속적인 저장공간(persistently store data) 등을 보충해 준다. 글이 딱딱해 지는 것 같은데, 우선 CLDC의 내용부터 살펴보자. CLDC(Connected, Limited Device Configuration) CLDC에서 정의하는 요소는 다음과 같다.
  • 자바 언어와 가상머신
  • 코어 자바 라이브러리
  • 입출력
  • 네트워킹
  • 보안
CLDC에서 정의하지 않는 것은 다음과 같다.
  • 응용프로그램의 생명주기
  • 유저 인터페이스
  • 이벤트처리
  • 고급 응용프로그램 모델
CLDC의 특징 J2ME를 하면서 가장 관심이 가는 부분은 J2SE의 어느 부분이 지원되고 어느 부분이 삭제되었는지이다. CLDC에서는 다음과 같은 J2SE의 부분이 변경되었다. 애플리케이션 관리(Application management) 대부분의 작고 자원이 한정된 디바이스에는 파일 시스템이나 동적으로 다운로드하는 정보를 저장하는 다른 표준적인 메커니즘이 없다. 그래서 CLDC는 외부로부터 디바이스 내에 영속적으로 저장하기 위한 자바 클래스를 지원하지 않는다. 단지 자바 가상머신은 클래스 파일을 로드하거나 로드 후에 버린다. 이런 이유로 애플리케이션 관리는 디바이스와 구현 중심의 문제가 되어 버린다. 대부분 C 프로그래밍 언어나 다른 저수준 언어로 애플리케이션 관리(Applecation management)를 제공한다. 보안(Security) CLDC에서는 보안은 저수준 가상머신 보안과 애플리케이션 수준의 보안이라는 두가지 영역으로 나누어 진다. 저수준 가상머신 보안은 자바 애플리케이션이 자바 가상머신에서 실행될 때 디바이스에 해를 끼치지 않게 하는 것을 말한다. 기존의 자바 가상머신은 자바 클래스 파일 검증기(Java Classfile verifier)에서 보증하지만, CLDC에서는 사전검증기(preverifier)을 통해 자바 클래스를 검증한다. 표준 자바의 보안 모델은 CLDC에 사용하기에는 메모리 소비가 너무 많다. 그래서 애플리케이션수준의 보안은 단순한 모래상자(Sandbox model)를 사용한다. 모래상자는 블랙박스처럼 CLDC에서 정의된 API만 사용할 수 있으며, 클래스 로딩과 VM 클래스 오버라이딩을 금지한다. 그리고 당연하겠지만 네이비트 함수를 호출하는 라이브러리를 새로 작성할 수 없다. 자바 언어에서 변한 점 이제 자바 언어에서 어떤 점이 변화했는지 알아보자. 부동 소수점(floating point): 지원 안함 우선 지원하는 하드웨어에서 부동 소수점을 지원 하지 않으며, 소프트웨어에서도 오버헤드가 크다. finalization: 지원 안함 즉, Object.finalize()가 없다는 뜻이다. 에러 처리 지원 제한 CLDC에서는 제한적인 에러 처리만을 지원한다. 다음은 지원하는 클래스들이다.
  • java.lang.error
  • java.lang.VirtualMachineError
  • java.lang.OutOfMemoryError
정리를 한다면 자바 언어에서 가장 크게 변한 것은 부동 소수점을 지원 하지 않는다는 것, float이나 double과 같은 부동 소수점 리터럴 형을 사용 할 수 없는 것이다. JNI 사용 불가 자바 가상 머신에도 역시 변경된 부분이 많은데, 간단히 살펴보면 JNI(Java native interface)를 지원하지 않으며, 사용자-지원 클래스 로더를 작성할 수 없다는 것 등인데 이것은 보안적인 측면에서의 변경이라고 볼수 있다. 그리고 리플렉션(reflection)과 스레드 그룹, 데몬 스레드 작성 불가, 약한 참조(weak reference) 등은 성능상의 이유로 삭제되었다고 볼 수 있겠다. 사전검증(preverification) 그리고 자바 가상머신 부분에서 가장 눈에 띄는 변화는 역시 사전 클래스 검증 메커니즘일텐데, 간단하게 요약하자면 탑재 전의 사전검증(preverification)과 탑재 후의 검증(verification)으로 나누어 졌다. 검증 부분을 둘로 나눈 이유는 J2SE가 보통 50KB의 바이너리 코드 30~100KB정도의 RAM을 사용하는 검증 모듈을 CLDC의 모듈에 탑재 하지 않음으로써 자원을 효율적으로 사용하고, 네트워크를 통해 전송하기 전에 보안성을 일차적으로 보장함으로써 보안성을 높일 수 있기 때문이다. 그리고 자바 클래스의 배포에 있어서 Jar 포맷을 사용하는데 JAR 압축으로 30-50%의 대역폭을 절감할 수 있다. CLDC API 다음 중에 무엇이 바뀌고 무엇이 새로 첨가 되었나?
  • java.io: 데이터 스트림을 통해 시스템 입출력
  • java.lang: 자바 언어의 가장 기본적인 클래스 제공
  • java.util: 컬렉션, 날짜, 시간등 부가 클래스 제공
J2SE에서의 재사용한 클래스는 위의 3가지 뿐이다. 그리고 다음 클래스가 추가되었다.
  • javax.microedition.io: GCF(Generic Connection Framework) 제공
GCF는 MIDP의 설명이 끝난 후 다시 이야기하도록 하자. MIDP(Mobine Information Device Profile) CLDC위에 MIDP만이 올라올 것 같지만 사실상 MIDP 애플리케이션 말고도 OEM 애플리케이션이 올라 올 수 있도록 OEM 전용 클래스를 정의할 수 있게 했다. MIDP의 구조 MIDP에서는 다음을 정의하고 있다.
  • 애플리케이션 모델
  • 유저 인터페이스와 이벤트 핸들링
  • 영속적 저장공간
  • 네트워킹
  • 타이머 지원
애플리케이션 모델-MIDlet MIDlet은 애플리케이션 생명주기를 관장하는 패키지이며, 애플릿과 비슷하다. 다음은 MIDlet 생명주기를 자바 가상 기계의 속도의 측량을 간결하게 하기 위해 사용하는 예제이다.

import javax.microedition.midlet.*;
import java.lang.String;
public class MethodTimes extends MIDlet implements Runnable {
Thread thread;
public void startApp() {
        thread = new Thread(this);
        thread.start();
        System.out.println("startApp");
    }
public void pauseApp() {
        thread = null;
        System.out.println("pauseApp");
    }
public void destroyApp(boolean unconditional) {
        thread = null;
        System.out.println("destroyApp");
    }
public void run() {
        Thread curr = Thread.currentThread();  
        long start = System.currentTimeMillis();
        for (int i = 0; i < 1000000 && thread == curr; i++) {
            empty();
        }
        long end = System.currentTimeMillis();
                
if (thread != curr) {
            return;
        }
        long millis = end - start;
                  System.out.println("Elapsed " + millis + " ms.");
                   destroyApp(true);
        notifyDestroyed();
    }
void empty() {
    }
}
MIDlet은 애플리케이션 생명주기를 관장하므로 작성하는 MIDlet에 다음과 같은 메소드를 작성해야 한다.
  • pauseApp-MIDlet: 얼마간의 임시 자원들을 풀어야 하고 수동적이다.
  • startApp-MIDlet: 자원이 필요하고, 다시 시작하는 얼마간의 자원들을 얻어야 한다.
  • destroyApp-MIDlet: 상태를 저장하며, 모든 자원을 푼다.
이외에도 애플리케이션 생명주기에 관련된 인터페이스는 다음과 같다.
  • notifyDestroyed-MIDlet: 적용 관리 소프트웨어에게 그것이 치웠고 그리고 된다고 알린다.
  • notifyPaused-MIDlet: 적용 관리 소프트웨어를 그것이 멈춘 것에게 알린다.
  • resumeRequest-MIDlet: 적용 관리 소프트웨어가 다시 시작되도록 요구한다.
RMS(Record Management System) 레코드 관리 시스템(Record Management System)은 지속적인 저장(Persistent Storage)을 위해서 만들어진 메커니즘이다. 일반적인 데스크탑과 달리 파일 시스템이 없기 때문에 MID의 리부팅이나 배터리 교환 등에 있어서 영속적으로 저장할 데이터를 저장하는 메커니즘이다. 레코드 스토어(Record Store)는 레코드(Record)의 집합으로 구성되어 있다. 레코드 스토어(Record Store)는 플랫폼 종속적인 부분에 위치해 있으며 MIDlet에 직접적으로 노출되어 있지는 않다. 하지만 MIDlet 수트안의 MIDlet은 다중 레코드 스토어를 생성 할수 있다. MIDlet 수트가 플랫폼에서 제거될 때 MIDlet 수트의 MIDlet과 연관된 레코드 스토어는 사라진다. 레코드 스토어(Record store)의 이름공간은 MIDlet 수트에 의해 컨트롤되며, 레코드 스토어 이름은 MIDlet 수트에서의 영역에서 절대적인 것이다. 다시 말해 MIDlet 수트안의 MIDlet은 같은 이름의 레코드 스토어를 만들수 없다. 레코드(Record)는 바이트 배열이다. recordId는 레코드의 프라이머리 키인데, 예를 들어 처음 레코드 스토어 안에 레코드가 생성되면서 recordId는 1이 대입된다. 2개의 레코드가 레코드 스토어에 더해지면 recordId는 3이 된다. MIDlet은 RecordEnumeration 클래스를 이용해서 다른 인덱스를 만들 수도 있다. RMS의 패키지는 다음과 같다.
interface RecordComparator interface RecordEnumeration Interface RecordFilter Interface RecordListener Class RecordStore
5개의 예외 처리도 있다.
InvalidRecordIDException RecordStoreException RecordStoreFullException RecordStoreNotFoundException RecordStoreNotOpenException
RMS를 이용한 게임 스코어를 저장하는 MIDlet 예제를 보자.

import javax.microedition.midlet.*;
import javax.microedition.rms.*;
import java.io.DataOutputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ByteArrayInputStream;
import java.io.DataInputStream;
import java.io.EOFException;

public class RMSGameScores extends MIDlet 
implements RecordFilter, RecordComparator
{

 private RMSGameScores rmsgs = null;
 private RecordStore recordStore = null;
 public static String playerNameFilter = null;
 
 
 public boolean matches(byte[] candidate)
        throws IllegalArgumentException
 {
        if (this.playerNameFilter == null) {
            return false;
        }
        ByteArrayInputStream bais = new ByteArrayInputStream(candidate);
        DataInputStream inputStream = new DataInputStream(bais);
        String name = null;
                try {
            int score = inputStream.readInt();
            name = inputStream.readUTF();
        }
                catch (EOFException eofe) {
            System.out.println(eofe);
            eofe.printStackTrace();
        }
        catch (IOException eofe) {
            System.out.println(eofe);
            eofe.printStackTrace();
        }
        return (this.playerNameFilter.equals(name));
    }
        
        public int compare(byte[] rec1, byte[] rec2)
        {
        ByteArrayInputStream bais1 = new ByteArrayInputStream(rec1);
        DataInputStream inputStream1 = new DataInputStream(bais1);
        ByteArrayInputStream bais2 = new ByteArrayInputStream(rec2);
        DataInputStream inputStream2 = new DataInputStream(bais2);
                
        int score1 = 0;
        int score2 = 0;
                
        try {
            
            score1 = inputStream1.readInt();
            score2 = inputStream2.readInt();
        }
        catch (EOFException eofe) {
            System.out.println(eofe);
            eofe.printStackTrace();
        }
        catch (IOException eofe) {
            System.out.println(eofe);
            eofe.printStackTrace();
        }       
        if (score1 < score2) {
            return RecordComparator.PRECEDES;
        }
        else if (score1 > score2) {
            return RecordComparator.FOLLOWS;
        }
        else {
            return RecordComparator.EQUIVALENT;
        }
    }

        public RMSGameScores()
        {
        try {
            recordStore = RecordStore.openRecordStore("scores", true);
        }
        catch (RecordStoreException rse) {
            System.out.println(rse);
            rse.printStackTrace();
        }
    }

        public void addScore(int score, String playerName)
         {
         ByteArrayOutputStream baos = new ByteArrayOutputStream();
        DataOutputStream outputStream = new DataOutputStream(baos);
        try {
                         outputStream.writeInt(score);
          
            outputStream.writeUTF(playerName);
        }
        catch (IOException ioe) {
            System.out.println(ioe);
            ioe.printStackTrace();
        }

        byte[] b = baos.toByteArray();
        try {
            recordStore.addRecord(b, 0, b.length);
        }
        catch (RecordStoreException rse) {
            System.out.println(rse);
            rse.printStackTrace();
        }
    }

        private void printScoresHelper(RecordEnumeration re)
        {
        try {
            while(re.hasNextElement()) {
                int id = re.nextRecordId();
                ByteArrayInputStream bais = new ByteArrayInputStream(recordStore.getRecord(id));
                DataInputStream inputStream = new DataInputStream(bais);
                try {
                    int score = inputStream.readInt();
                    String playerName = inputStream.readUTF();
                    System.out.println(playerName + " = " + score);
                }
                catch (EOFException eofe) {
                    System.out.println(eofe);
                    eofe.printStackTrace();
                }
            }
        }
        catch (RecordStoreException rse) {
            System.out.println(rse);
            rse.printStackTrace();
        }
        catch (IOException ioe) {
            System.out.println(ioe);
            ioe.printStackTrace();
        }
    }


        public void printScores()
        {
        try {
          
            RecordEnumeration re = recordStore.enumerateRecords(null, this, true);
            printScoresHelper(re);
        }
        catch (RecordStoreException rse) {
            System.out.println(rse);
            rse.printStackTrace();
        }
    }

        public void printScores(String playerName)
        {
        try {
            // Enumerate the records using the comparator and filter
            // implemented above to sort by game score.
            RecordEnumeration re = recordStore.enumerateRecords(this, this, true);
            printScoresHelper(re);
        }
        catch (RecordStoreException rse) {
            System.out.println(rse);
            rse.printStackTrace();
        }
        }
        public void main()
        {
        rmsgs = new RMSGameScores();
        rmsgs.addScore(100, "Alice");
        rmsgs.addScore(120, "Bill");
        rmsgs.addScore(80, "Candice");
        rmsgs.addScore(40, "Dean");
        rmsgs.addScore(200, "Ethel");
        rmsgs.addScore(110, "Farnsworth");
        rmsgs.addScore(220, "Farnsworth");
        System.out.println("All scores");
        rmsgs.printScores();
        System.out.println("Farnsworth"s scores");
        RMSGameScores.playerNameFilter = "Farnsworth";
        rmsgs.printScores("Farnsworth");
        }
        
        public void startApp() throws MIDletStateChangeException{
        System.out.println("startApp() - invoking main");
   
          main();
        }
        public void pauseApp(){
        System.out.println("pauseApp()");
        }
        public void destroyApp(boolean cond) {
        System.out.println("destroyApp()");
        try{
        if(recordStore != null)
        recordStore.closeRecordStore();
        }catch (Exception ignore){

    }
  }


}
주요 메소드를 우선 살펴보면 RMSGameScores을 생성하여 스코어를 더하고(addScores), 스코어를 출력하고(printScores), 어느 한 플레이어의 스코어를 RMS에서 찾아서(playerNameFilter) 출력하는(printScores)하는 기본 구조를 가지고 있다. MIDlet의 startApp()에서 main()메소드를 호출하고, pauseApp(), destroyApp(boolean cond)에서 RecordStore를 닫는다. RMSGameScores는 RecordFilter, RecordComparator을 구현하는데, RecordFilter의 matches는 playerName이 있는지 체크하고 RecordComparator의 compare는 두개의 레코드를 비교 한다. RMSGameScores생성자에서 레코드 스토어를 연 다음 레코드을 얻기 위해

        ByteArrayInputStream bais = new ByteArrayInputStream(candidate);
        DataInputStream inputStream = new DataInputStream(bais);
        String name = null;               
        int score = inputStream.readInt();
        name = inputStream.readUTF();
으로 값을 얻어낸다. printScores()과 printScores(String playerName)가 있는데 이곳에서 comparator과 filter로 레코드를 열거하고 printScores의 helpermethod인 printScoresHelper에서 열거된 레코드을 출력한다. GCF(Generic Connection Framework) GCF는 J2SE에 없는 새로운 커넥션 프레임워크이다. 이것은 자바의 추상화 프레임워크로 다양한 프로토콜(TCP/IP, WAP, iMode, IrDA, Bluetooth)에 대한 하나의 추상화 계층을 말한다. 그리고 다음과 같은 설계 목표가 있다.
  • 서로 다른 형태의 입출력 형태를 일관성있게 지원한다.
  • 서로 다른 형태의 프로토콜을 일관성있게 지원한다.
  • 애플리케이션의 포터빌리티를 향상시킨다.
  • 표준 자바 클래스 라이브러리와의 상호 호환성을 가진다.
  • 더 작은 메모리 풋프린트를 가진다
커넥션은 다음과 같은 방식으로 한다.

Connector.open(“:
”);
모든 커넥션을 이와 같은 방법으로 하지만 내부에서 작용하는 메소드는 그 프로토콜에서 달라질 수밖에 없다. Connector 클래스만으로 모든 연결을 생성할 수 있으며, open() 메소드의 인자를 변경함으로써 연결의 형태를 바꿀 수 있다는 데에 있다. 연결이 성공적이었다면, open() 메소드는 커넥션 프레임워크의 인터페이스를 구현한 클래스의 인스턴스를 반환한다. CLDC에서는 Connector과 실제 구현에 대한 명세를 포함하지 않고 MIDP와 같은 프로파일에서 정의를 하도록 하고 있다.


그림 GCF의 구조

그럼 javax,microedition.io을 잠시 살펴보자.
Connection Datagram DatagramConnection InputConnection OutputConnection StreamConnection ContentConnection StreamConnectionNotifier DatagramConnection
그리고 프로토콜별 연결 예이다.
HTTP Connector.open("http://www.foo.com”) FTP Connector.open(“ftp://www.foo.com”); 소켓 Connector.open(“socket://129.144.111.222:9000”) 커뮤니케이션 포트 Connector.open(“comm:0;baudrate=9600”) 데이터그램 Connector.open(“datagram://129.144.111.333”) 파일 Connector.open(“file:/foo.dat”)
그런데 가장 물리적인 부분 하나가 하나가 빠졌다.
javax.microedition.io.HttpConnection
HttpConnection을 이용한 GCF예제를 한번 보자.

void getViaHttpConnection(String url) throws IOException {
         HttpConnection c = null;
         InputStream is = null;
         try {
             c = (HttpConnection)Connector.open(url);
            // HTTP 헤더와 커넥션 열기 위해 inputStream을 얻는다.           
             is = c.openInputStream();            
             //컨텐츠타입을 얻는다.
             String type = c.getType(); 
             // Get the length and process the data
             int len = (int)c.getLength();
             if (len > 0) {
                 byte[] data = new byte[len];
                 int actual = is.read(data);
                 ...
             } else {
                 int ch;
                 while ((ch = is.read()) != -1) {
                     ...
                 }
             }
         } finally {
             if (is != null)
                 is.close();
             if (c != null)
                 c.close();
         }
Connector.open을 이용하여 URL을 열고 HttpConnection을 리턴한다. HTTP헤더는 읽고 처리된다. HttpConnection으로부터 inputStream이 열린다. MIDP ui=lcdui 데스크탑 시스템과 달리 MID와 같은 디바이스는 UI 형태가 다르며, 보통 인터페이스가 매우 제한되어 있다. MIDP에서는 기존의 J2SE에서의 AWT를 사용하지 않고 javax.microedition.lcdui라는 UI API를 사용한다.AWT는 윈도우 관리(다중 윈도우창, 윈도우 크기 재조정)와 같은 풍부한 API set을 가지고 있지만, MID과 같은 제한된 인터페이스에서는 그런 API가 필요하지 않다. 그리고 AWT는 사용자 반응 모델이라고 볼 수 있다. 즉 포인터 디바이스(마우스나 펜 포인트)를 위해 설계되었다. 그러나 많은 MID는 이런 디바이스가 아니다. 그리고 중요한 점은 AWT에서의 사용자 반응이 일어날 때 이벤트 객체는 동적으로 생성된다. 이벤트가 처리되고 GC에 의하여 제거되는데, 제한된 CPU와 메모리를 가진 시스템에서는 이런 작업이 부담스러울 수 있다. 참고로 java.util 패키지에는 EventObject클래스와 EventListener 인터페이스가 없다. MIDP UI API는 두가지로 구분할 수 있는데, 높은 수준의 이식성을 요구하는 응용을 위한 API, 즉 고급 API과 세부적인 그래픽을 표현하거나 입력장치를 제어하는 등의 정밀한 제어가 가능한 API, 즉 저급 API가 있다. 이식성을 요구하는, 즉 비지니스 애플리케이션을 작성하기 위해 만든 고급 API는 추상화 정도는 높고 룩앤필(look&feel)은 낮다. 즉 컴포넌트의 기본적인 상호작용(네비게이션, 스크롤 등)에 관하여 응용 애플리케이션을 알 수가 없고, 컴포넌트의 비주얼한 부분(모양,폰트,색)을 제어할 수 없다. 그리고 특정한 입력 디바이스에 대한 접근이 불가능하다. 게임 등에 쓰이는 저급 API는 추상화 정도가 낮다. 그래서 많은 제어가 가능하지만 그만큼 해주어야 할 것도 많다. 기본적으로 지원하는 추상화는 정확한 배치, 그래픽 요소의 컨트롤 등이 있다. 위에서 고급 API와 저급 API의 특징에 대하여 말했는데, 이제 그 두 계층에 해당하는 클래스를 살펴보자. 고급 API의 최상위 추상 클래스인 screen은 텍스트 박스, 리스트 등의 상위 레벨 API를 위한 최상위 추상화를 제공한다. 이벤트에도 고급과 저급이 있는데 고급 이벤트인 command 클래스는 추상 커맨드와 소프트 키를 통한 이벤트 핸들링이다.

 void commandAction(Command c, Screen s);
그럼 다시 HelloMIdlet으로 가 보자.

import javax.microedition.midlet.*;
import javax.microedition.lcdui.*;

public class HelloMIDlet extends MIDlet implements CommandListener {

    private Command exitCommand; 
    private Display display;    

    public HelloMIDlet() {
        display = Display.getDisplay(this);
        exitCommand = new Command("Exit", Command.SCREEN, 2);
    }
    public void startApp() {
        TextBox t = new TextBox("Hello MIDlet", "Test string", 256, 0);

        t.addCommand(exitCommand);
        t.setCommandListener(this);

        display.setCurrent(t);
    }
    public void pauseApp() {
    }

  public void destroyApp(boolean unconditional) {
    }
  public void commandAction(Command c, Displayable s) {
        if (c == exitCommand) {
            destroyApp(false);
            notifyDestroyed();
        }       
    }

}
이 예제는 TextBox 스크린을 하나 생성하고, 여기에 Exit라는 추상 커맨드를 추가한 이후에 사용자가 종료 커맨드를 선택하면, 해당하는 이벤트에 대한 액션으로 프로그램의 수행을 종료하는 기본적인 기능만을 수행한다. 신명수님은 한빛 리포터 1기로 활동 중이며, 자바와 리눅스에 관심이 많습니다.
TAG :
댓글 입력
자료실

최근 본 상품0