BLOG ARTICLE NSView | 3 ARTICLE FOUND

  1. 2007.08.14 1.8 간단한 슈팅게임 (1) (4)
  2. 2007.05.16 1.6 SimpleViewer 이미지 뷰어 (1) (2)
  3. 2007.05.11 1.5 NSView를 이용한 그리기 (7)

1.8.0 프로젝트 개요
이번 장에서는 NSView와 NSImage를 이용해서 간단한 슈팅게임을 만들어 보겠습니다.

사용자 삽입 이미지
좌측과 같이 배경을 출력하고, 우주선을 좌우로 조종하고, 미사일을 발사하는  작업까지 하겠습니다.

키입력에서 다중으로 입력 받을 수가 없어 이동시에 미사일을 발사하면 이동이 중지 됩니다. 이문제는 다음 장에서 해결하도록 하겠습니다.





1.8.1 프로젝트 생성
Xcode를 실행하고 아래와 같이 SimpleShot 코코아 프로젝트를 생성합니다.
  1. 메뉴바에서 File/New Project...를 선택합니다.
  2. Application/Cocoa Application을 선택합니다.
  3. project Name에 SimpleShot를 입력하고 finish 버튼을 클릭합니다.

1.8.2 인터페이스 빌더에서 작업
1) 서브 클래스 생성

사용자 삽입 이미지
Nib 파일을 클릭하여 인터페이스빌더를 열고 NSView 클래스의 서브클래스를 만들고(아래의 창에서 NSView에 마우스 우클릭 합니다. 그 다음 Subclass NSView 클릭) 이름을 StageView로 변경합니다


사용자 삽입 이미지
StageView를 우클릭 하고 좌측과 같이 Create Files for StageView를 클릭하여 소스파일을 생성합니다.





2) NSView 생성 및 속성 설정

윈도우를 열고 팔레트의 Containers 항목에서  CustomeView를 드래그하여 아래와 같이 윈도우에 배치 합니다. View의 크기를 300X320으로 변경하고,  Custom Class 항목에서 StageView로 선택합니다.
사용자 삽입 이미지사용자 삽입 이미지
(클릭하면 이미지가 확대됩니다.)

1.8.3 이미지 파일

1) 이미지 생성

이제 배경, 우주선, 미사일에 쓰일 이미지를 준비합니다. 배경은 상관없지만 우주선과 미사일은 윤곽선이외에는 투명색으로 보여야 하기 때문에 png를 사용했습니다.

사용자 삽입 이미지
저는 도스의 디럭스페인트 이후로는 그래픽 작업이 불가능하므로 초등학생인 아들에게 이미지를 부탁하였습니다. 그래픽툴들을 다루실 수 있는 분들은 새로 만드셔도 되고, 아니면 첨부파일에서 다운로드 받으셔서 사용하시면 됩니다.

좌측은 배경화면 이미지입니다. StageView와 같은 300X320의 크기를 가지고 있습니다.



사용자 삽입 이미지
우주선 이미지 입니다. 하얗게 보이는 부분을 투명하게 처리해야 하단의 배경화면에서 올바르게 보입니다. 이를 위해 png파일 포맷을 사용했습니다.

사용자 삽입 이미지
미사일 이미지 입니다. 위와 같이 png파일 포맷을 사용했습니다.

2) 이미지 등록

사용자 삽입 이미지
준비된 이미지 파일을 드래그하여 Xcode 좌측의  Group & Files 목록의 Resources에 가져다 놓습니다.



드래그 시 나오는 판넬에서 아래와 같이 Copy items... 항목의 체크를 확인합니다.
사용자 삽입 이미지

1.8.4 소스코드 작성

1) StageView.h 수정

#import <Cocoa/Cocoa.h>

@interface StageView : NSView
{
    NSTimer *timer;
    NSImage *backgroundImage;
   
    NSRect heroRect;
    NSImage *heroImage;

    BOOL isFire;
    NSRect missileRect;
    NSImage *missileImage;
}

- (void)processGame;
- (void)fireMissile;
- (void)keyDown:(NSEvent *)event;

@end


2) StageView.m 수정

#import "StageView.h"

#define SCREEN_WIDTH        300
#define SCREEN_HEIGHT        320

#define HERO_SPEED            3       /* 우주선 속도 */
#define MISSILE_SPEED        4       /* 총알발사 속도 */

@implementation StageView

- (id)initWithFrame:(NSRect)frameRect
{
    if ((self = [super initWithFrame:frameRect]) != nil) {
        // Add initialization code here
       
        isFire = NO;
       
        /* 배경 이미지 로드 */
        NSString* imageName = [[NSBundle mainBundle] pathForResource:@"background" ofType:@"png"];
        backgroundImage = [[NSImage alloc] initWithContentsOfFile:imageName];

        /* 우주선 이미지 로드 & 초기 좌표 설정 */
        imageName = [[NSBundle mainBundle] pathForResource:@"hero" ofType:@"png"];
        heroImage = [[NSImage alloc] initWithContentsOfFile:imageName];
        heroRect.size = [heroImage size];
        heroRect.origin = NSMakePoint((SCREEN_WIDTH - heroRect.size.width)/2, 0);
       
        /* 미사일 이미지 로드 & 초기 좌표 설정 */
        imageName = [[NSBundle mainBundle] pathForResource:@"missile" ofType:@"png"];
        missileImage = [[NSImage alloc] initWithContentsOfFile:imageName];
        missileRect.size = [missileImage size];
        missileRect.origin = NSMakePoint(heroRect.origin.x + heroRect.size.width/2 - missileRect.size.width/2, heroRect.size.height);
       
        /* 30프레임으로 타이머 설정 */
        timer = [[NSTimer scheduledTimerWithTimeInterval: (1.0f / 30.0f)
                                                  target: self
                                                selector:@selector(processGame)
                                                userInfo:self
                                                 repeats:true] retain];
    }
    return self;
}

- (void) dealloc
{
    [backgroundImage release];
    [heroImage release];
    [missileImage release];
       
    [super dealloc];
}

/* 키입력을 받기 위한 설정 */
- (BOOL)acceptsFirstResponder
{
    return YES;
}

- (void)keyDown:(NSEvent *)event
{
    int keyCode;
    
    /* 현재 눌려진 키값을 얻어 온다. */
    keyCode = [event keyCode];
   
    /* 좌측으로 이동 */
    if(keyCode == 123)
    {
        heroRect.origin.x -= HERO_SPEED;
        if(heroRect.origin.x < 0)
            heroRect.origin.x = 0;
    }

    /* 우측으로 이동 */
    if(keyCode == 124)
    {   
        heroRect.origin.x += HERO_SPEED;

        if(heroRect.origin.x >= SCREEN_WIDTH - heroRect.size.width)
            heroRect.origin.x = SCREEN_WIDTH - heroRect.size.width;
    }
   
    /* 발사(스페이스) */
    if(keyCode == 49)
    {
        [self fireMissile];
    }
}       

- (void)fireMissile
{
    if(isFire == NO)
    {
        /* 미사일이 처음 발사되었을 경우에 초기위치를 우주선 좌표로 설정 */
        missileRect.origin = NSMakePoint(heroRect.origin.x + heroRect.size.width/2 - missileRect.size.width/2, heroRect.size.height);
    }
    isFire = YES;
}

- (void)processGame
{
    if(isFire == YES)
    {
        /* 미사일이 발사중이면 y좌표를 이동 */
        missileRect.origin.y += MISSILE_SPEED;
        if(missileRect.origin.y >= SCREEN_HEIGHT)
        {
            /* 화면상단에 위치했을 경우에는 미사일을 출력하지 않는다 */
            isFire = NO;
        }
    }
   
    [self setNeedsDisplay:YES];
}

- (void)drawRect:(NSRect)rect
{
    NSRect imgRect;
    NSRect drawRect;
   
    /* 배경 이미지 출력 */
    imgRect.origin = NSZeroPoint;
    imgRect.size = [backgroundImage size];
   
    drawRect = [self bounds];
   
    [backgroundImage drawInRect:drawRect
                     fromRect:imgRect
                    operation:NSCompositeSourceOver
                     fraction:1.0];
   
    /* 우주선 출력 */
    imgRect.origin = NSZeroPoint;
    imgRect.size = [heroImage size];
   
    [heroImage drawInRect:heroRect
               fromRect:imgRect
              operation:NSCompositeSourceOver
               fraction:1.0];   
   
    if(isFire == YES)
    {
        /* 미사일 출력 */
        imgRect.origin = NSZeroPoint;
        imgRect.size = [missileImage size];
       
        [missileImage drawInRect:missileRect
                     fromRect:imgRect
                    operation:NSCompositeSourceOver
                     fraction:1.0];   
    }   
}

@end

#define HERO_SPEED            3       /* 우주선 속도 */
#define MISSILE_SPEED        4       /* 미사일발사 속도 */

한 프레임에서 우주선과 총알의 움직이는 속도를 설정합니다. 좌/우 방향키를 누르면 각각의 방향으로 우주선이 한 프레임당 3픽셀을 움직입니다. 미사일은 4픽셀씩 상단으로 이동합니다. 숫자를 바꾸어 주면 해당 오브젝트의 속도를 변경할 수 있습니다.

StageView가 생성될 때 자동으로 호출되는 - (id)initWithFrame:(NSRect)frameRect 메소드에서 이미지 파일을 로드하고 타이머를 세팅하는 작업을 합니다.

NSString* imageName = [[NSBundle mainBundle] pathForResource:@"background" ofType:@"png"];
backgroundImage = [[NSImage alloc] initWithContentsOfFile:imageName];

어플리케이션의 리소스 디렉토리에서 background.png 이미지 파일을 로드하고 NSImage를 생성 합니다. 우주선과 미사일 이미지를 차례로 로드합니다.

timer = [[NSTimer scheduledTimerWithTimeInterval: (1.0f / 30.0f)
                                                  target: self
                                                selector:@selector(processGame)
                                                userInfo:self
                                                 repeats:true] retain];

1/30초 마다 processGame 함수를 호출하도록 타이머를 설정합니다. processGame 함수에서는 [self setNeedsDisplay:YES];로 설정하여 DrawRect가 호출되어 이미지를 출력하게 합니다.
이제 컴파일을하고 실행을 합니다. 좌우 방향키로 우주선을 움직이며 스페이스키로 미사일을 발사할 수 있습니다. 미사일은 현재 한발씩만 발사 할 수 있습니다. 다음 장에서는 적기들을 만들고 움직여 보도록 하겠습니다.

1.6.0 프로젝트 개요
JPG, GIF등의 이미지 파일을 NSImage를 이용하여 화면에 출력하고 , 확대 축소를 해주는 간단한 이미지 뷰어 프로그램을 만들어 보겠습니다.
사용자 삽입 이미지

1.6.1 프로젝트 생성
이전 포스트에서 경험 해 본 코코아 프로젝트와 소스파일을 생성하고, 인터페이스 빌더와 연결 할 수 있다는 전제 하에 설명하겠습니다. 이 부분에 이해가 안되시면 1.2와 1.3 포스트를 해보시고 오시기 바랍니다.

Xcode를 실행하고 아래와 같이 SimpleDraw 코코아 프로젝트를 생성합니다.
  1. 메뉴바에서 File/New Project...를 선택합니다.
  2. Application/Cocoa Application을 선택합니다.
  3. project Name에 SimpleViewer를 입력하고 finish 버튼을 클릭합니다.

1.6.2 인터페이스 빌더에서 작업

1) 서브 클래스 생성

Nib 파일을 클릭하여 인터페이스빌더를 열고 NSView 클래스의 서브클래스를 만들고(아래의 창에서 NSView에 마우스 우클릭 하여 Subclass NSView 클릭) 이름을 ImgView로 변경합니다.
사용자 삽입 이미지

위와 같은 방법으로 AppController란 NSObject의 서브클래스를 만듭니다. 서브클래스를 만든 후에 AppController를 우클릭하여 나오는 메뉴에서 Instantiate AppController를 클릭하여 인스턴스를 생성합니다.
사용자 삽입 이미지


2) 컨트롤 생성 및 속성 설정

윈도우를 열고 팔레트의 Containers 항목에서  CustomeView와 Controls 항목에서 Slider를 드래그해서 아래와 같이 윈도우에 배치 합니다.

CutomeView를 클릭 후에 인스펙터(command+5)를 불러 내어 Custom Class 항목에서, 이전에 만들어 놓은 ImgView를 선택하고 아래와 같이 CustomeView가 ImgView로 변경됨을 확인합니다.
사용자 삽입 이미지

Size 항목에서 하단의 Autosizing 항목에서 아래와 같이 사각형내의 선들을  마우스로 클릭하여 스프링처럼 나오게 만듭니다. 이 작업은 윈도우의 크기가 변경될 때 상하좌우 크기가 윈도우에 맞게 자동으로 변경되게 합니다.
사용자 삽입 이미지

슬라이더의 속성을 아래와 같이 설정 합니다. Minimum은 최소값, Maximum은 최대값, Current는 현재값, Number of Markers는 슬라이더 상단의 눈금의 갯수를 의미합니다.
사용자 삽입 이미지

슬라이더도 윈도우의 크기에 맞추어 변경되게 하기위해, 아래와 같이 Size 항목에서 하단의 Autosizing의 사각형 내부의 좌우선을 클릭하여 스프링이 되게 만듭니다. 이 작업은 슬라이더의 높이는 유지하며 너비가 윈도우의 크기와 함께 변하도록 만들어 줍니다.
사용자 삽입 이미지

3) 연결

사용자 삽입 이미지
인터페이스 빌더의 Instances항목에서 AppController의 인스펙터를 불러내어 imgView 아울렛을 추가하고, Type을 ImgView로 선택합니다.

인스펙터는 항목을 마우스로 클릭하여 포커스를 준 후에 shift+command+i 또는 commad+숫자 를 입력하여 불러낼 수 있습니다.

이 작업은 AppController에서 ImgView를 제어할 수 있도록 합니다.










사용자 삽입 이미지
이제 Actions 항목을 선택하고 resizeImage와 openImage 액션을 추가합니다.

resizeImage는 하단의 슬라이더가 변경될 때, 이미지 크기를 변경하고, openImage는 메뉴에서 파일을 오픈할 때 처리하기 위한 액션입니다.




control 키를 누른 상태에서 AppController를 드래그하여  ImgView에 놓습니다. 아래와 같이 AppControll의 ImgView 아울렛에 연결(connect 버튼을 클릭 후, 원모양 아이콘을 확인)합니다.
사용자 삽입 이미지

control 키를 누른 상태에서 슬라이더를 드래그하여 AppController에 놓습니다. Taget/Action에서 resizeImage에 연결합니다.
사용자 삽입 이미지

아래와 같이 메인메뉴의 File에서 오픈을 control 키를 누른 상태에서 드래그하여 AppController에 놓습니다. Target/Action에서 openImage에 연결합니다. 이 작업으로 사용자가 메뉴에서 Open을 클릭하면 AppController의 openImage를 호출합니다.
사용자 삽입 이미지

이제 인스펙트빌더에서 1차적인 작업이 완료되었습니다. Classes에서 AppController와 ImgView를 각각 우클릭하여 CreateFiles  for AppController(and Img View)를 선택하여 소스파일을 생성하고 Xcode로 돌아 갑니다.


1.6.3 소스코드 수정
1) AppController.h 수정

인터페이스 빌더에서의 작업으로 헤더파일에서 해야 할 작업은 거의 없습니다. 다만 컴파일 시 오류를 막기 위해 아래와 같이 @class ImgView; 란 라인을 추가합니다.
#import <Cocoa/Cocoa.h>

@class ImgView;

@interface AppController : NSObject
{
    IBOutlet ImgView *imgView;
}
- (IBAction)openImage:(id)sender;
- (IBAction)resizeImage:(id)sender;
@end


2) AppController.m 수정

#import "AppController.h"
#import "ImgView.h"

@implementation AppController

- (void)openPanelDidEnd:(NSOpenPanel *)openPanel
             returnCode:(int)returnCode
            contextInfo:(void *)x
{   
    NSString *path;
    NSImage *image;
   
    if (returnCode == NSOKButton) {
        path = [openPanel filename];
   
        NSImageRep *imgRep = [NSImageRep imageRepWithContentsOfFile:path];
        NSSize frameSize = [imgRep size];
       
        image = [[NSImage alloc] initWithContentsOfFile:path];
       
        [imgView setImage:image];
        [imgView setFrameSize:frameSize];
        [image release];
    }
}

- (IBAction)openImage:(id)sender
{
    NSOpenPanel *panel = [NSOpenPanel openPanel];
   
    [panel beginSheetForDirectory:nil
                             file:nil
                            types:[NSImage imageFileTypes]
                   modalForWindow:[imgView window]
                    modalDelegate:self
                   didEndSelector:
        @selector(openPanelDidEnd:returnCode:contextInfo:)
                      contextInfo:NULL];
}

- (IBAction)resizeImage:(id)sender
{
    [imgView setRatio:[sender floatValue]];
}

@end

#import "ImgView.h"
ImgView 클래스를 사용하기 때문에 위와 같이 ImgView.h를 인클루드 시킵니다.

아래의 메소드는 파일 오픈을 클릭하였을 경우, 파일선택 창에서 파일 선택이 완료 된 후에 불리어 지는 콜백 함수입니다. 이 메소는 파일오픈 판넬을 오픈할 때 등록하여 주며, 나중에 다시 설명하겠습니다.
- (void)openPanelDidEnd:(NSOpenPanel *)openPanel
             returnCode:(int)returnCode
            contextInfo:(void *)x
{   
    NSString *path;
    NSImage *image;

사용자가 파일 창에서 [열기] 버튼을 클릭하였을 경우에만 실행되도록 합니다.  
    if (returnCode == NSOKButton) {
        path = [openPanel filename];
   
파일 원본 크기를 알기 위해 NSImageRep 오브젝트를 사용합니다.
        NSImageRep *imgRep = [NSImageRep imageRepWithContentsOfFile:path];
        NSSize frameSize = [imgRep size];
     
NSImage에 이미지 파일을 등록합니다.  
        image = [[NSImage alloc] initWithContentsOfFile:path];
       
imgView에 이미지를 설정하고, imgView의 크기를 이미지의 원본크기로 변경합니다.
        [imgView setImage:image];
        [imgView setFrameSize:frameSize];
        [image release];
    }
}

다음은 사용자가 파일오픈을 선택하였을 때 호출되는 - (IBAction)openImage:(id)sender 메소드에서 파일선택 판넬을 오픈하기 위해 아래와 같이 추가합니다.
    NSOpenPanel *panel = [NSOpenPanel openPanel];
   
    [panel beginSheetForDirectory:nil
                             file:nil
                            types:[NSImage imageFileTypes]
                   modalForWindow:[imgView window]
                    modalDelegate:self
                   didEndSelector:
        @selector(openPanelDidEnd:returnCode:contextInfo:)
                      contextInfo:NULL];
모달(판넬이 떠있을 때는 닫기전에는 소유 윈도우는 제어되지 않습니다)로 뛰우며 선택 가능한 파일 종류, 소유 윈도우, 콜백함수를 설정합니다.

@selector(openPanelDidEnd:returnCode:contextInfo:) 이 부분에서 파일선택이 완료되면 위에서 작성한 openPanelDidEnd 메소드가 호출되도록 하여 줍니다.

[imgView setRatio:[sender floatValue]];
슬라이더가 변경되면 호출되는 resizeImage 메소드에서 imgView가 크기를 변경하도록 setRatio를 호출합니다. 이 메소드는 다음 ImgView 클랙스에서 작성되어질 것 입니다.

3) ImgView.h 수정

#import <Cocoa/Cocoa.h>

@interface ImgView : NSView
{
    NSImage *image;
    NSSize orgSize;
    float ratio;
}

- (void)setImage:(NSImage *)img;
- (void)setRatio:(float)r;

@end

NSImage *image;

이미지 출력을 위해 NSImage 클래스를 선언 합니다.

NSSize orgSize;

이미지의 원본 크기를 저장합니다.

float ratio;
이미지 확대/축소 비율을 저장합니다. 0이 최소, 20이 최대, 10이 원본크기를 출력합니다.

- (void)setImage:(NSImage *)img;
선택된 이미지를 image에 설정하는 메소드입니다. 파일메뉴에서 파일 선택 시 AppController에서 호출합니다.

- (void)setRatio:(float)r;
확대/축소 비율을 설정합니다. 슬라이더 변경 시 AppController에서 호출합니다.

4) ImgView.m 수정

#import "ImgView.h"

@implementation ImgView

- (id)initWithFrame:(NSRect)frameRect
{
    if ((self = [super initWithFrame:frameRect]) != nil) {
        // Add initialization code here
        ratio = 10;
    }
    return self;
}

- (void)drawRect:(NSRect)rect
{
    [[NSColor whiteColor] set];
    [NSBezierPath fillRect:rect];
   
    if(image) {
        NSRect imgRect;
        NSRect drawRect;
       
        if(ratio != 0.0f) {
            imgRect.origin = NSZeroPoint;
            imgRect.size = orgSize;
       
            drawRect = [self bounds];
       
            NSLog(@"drawRect: %f, %f", drawRect.size.width, drawRect.size.height);
           
            [image drawInRect:drawRect
                     fromRect:imgRect
                    operation:NSCompositeSourceOver
                     fraction:1.0];
        }   
    }
}

- (void)setImage:(NSImage *)img
{
    [img retain];
    [image release];
   
    image = img;
   
    orgSize = [image size];
   
    [self setNeedsDisplay:YES];
}

- (void)setRatio:(float)r
{
    NSSize viewSize;
    ratio = r;
   
    NSLog(@"RATIO: %f", ratio);
   
    viewSize.width = (orgSize.width * ratio)/10;
    viewSize.height = (orgSize.height * ratio)/10;
   
    [self setFrameSize:viewSize];
    [self setNeedsDisplay:YES];
}

- (void) dealloc
{
    [image release];
    [super dealloc];
}

@end

ratio = 10;
initWithFrame는 ImgView가 초기화될 때, 자동으로 호출되는 메소드 입니다. 이곳에서 초기 오브젝트를 생성하거나 변수를 초기화 합니다. 이미지 선택시 원본 크기로 보이게 하기 위해 ratio를 10으로 설정합니다.

다음은 drawRect 메소드 내에 아래의 라인을 추가합니다. drawRect는 ImgView가 그려져야 할 필요가 있을 때 자동으로 불려지는 메소드 입니다. 이곳에서 그려질 내용들을 처리 합니다.

[[NSColor whiteColor] set];
[NSBezierPath fillRect:rect];
배경색을 흰색으로 채웁니다.

이미지 오브젝트가 설정되었을 때만 출력합니다.
    if(image) {
        NSRect imgRect;
        NSRect drawRect;

확대비율이 0일때는 출력하지 않습니다.  
        if(ratio != 0.0f) {
            imgRect.origin = NSZeroPoint;
            imgRect.size = orgSize;
       
            drawRect = [self bounds];

이미지를 출력합니다.  
            [image drawInRect:drawRect
                     fromRect:imgRect
                    operation:NSCompositeSourceOver
                     fraction:1.0];
        }   
    }
}

아래의 setImgae에서는 사용자가 선택한 파일을 image에 설정합니다.
- (void)setImage:(NSImage *)img
{
    [img retain];
    [image release];
 
image를 설정하고 원본 사이즈의 크기를 저장합니다.
    image = img;
    orgSize = [image size];
다시 그려지도록 알려주며, 이 위의 drawRect가 호출됩니다.  
    [self setNeedsDisplay:YES];
}

아래의 setRatio에서는 사용자가 슬라이더의 변경시에, 크기를 변경해 주는 작업을 합니다.
- (void)setRatio:(float)r
{
    NSSize viewSize;
    ratio = r;

10을 원본 크기로 비율에 맞게 크기를 조절합니다.  
    viewSize.width = (orgSize.width * ratio)/10;
    viewSize.height = (orgSize.height * ratio)/10;

ImgView의 크기를 현재 배율에 맞게 변경합니다.  
    [self setFrameSize:viewSize];
다시 그려지도록 알려주며, 이 위의 drawRect가 호출됩니다.
    [self setNeedsDisplay:YES];
}

메모리에서 해제합니다.
- (void) dealloc
{
    [image release];
    [super dealloc];
}

이제 모든 변경사항을 저장하고 프로젝트를 빌드 후에, 이미지 파일을 선택하여 테스트 해봅니다. 이미지를 불러보고 윈도우의 크기를 변경해 봅니다. 아직 스크롤을 처리하지 않아 커다란 이미지는 제대로 보실 수 없을 것입니다. 이는 다음장에서 처리해 보겠습니다.


1.5.0  프로젝트 개요
이번에는 NSView를 이용하여 도형들과 텍스트를 출력하는 예제를 만들어 보겠습니다. 아래와 같이 각각의 버튼들을 클릭하면 각각의 내용들을 출력하는 간단한 프로그램 입니다.
사용자 삽입 이미지
사용자 삽입 이미지
사용자 삽입 이미지
사용자 삽입 이미지






1.5.1  프로젝트 생성

이전 포스트에서 경험 해 본 코코아 프로젝트와 소스파일을 생성하고, 인터페이스 빌더와 연결 할 수 있다는 전제 하에 설명하겠습니다. 이 부분에 이해가 안되시면 1.2와 1.3 포스트를 해보시고 오시기 바랍니다.

Xcode를 실행하고 아래와 같이 SimpleDraw 코코아 프로젝트를 생성합니다.
  1. 메뉴바에서 File/New Project...를 선택합니다.
  2. Application/Cocoa Application을 선택합니다.
  3. roject Name에 SimpleDraw를 입력하고 finish 버튼을 클릭합니다.

1.5.2 인터페이스 빌더에서 작업
1) NSView 서브클래스 만들기

사용자 삽입 이미지
Xcode에서 MainMenu.nib를 더블클릭하여 인터페이스 빌더를 실행합니다. 좌측과 같이 Classes 에서 NSObject > NSResponder > NSview를 우클릭(또는 control + 클릭) 하여 메뉴에서 Subclass NSView를 선택 합니다.


사용자 삽입 이미지
MyView라는 서브 클랙스가 생성됩니다. 여기서는 MyView라는 이름을 사용하지만, 용도에 맞는 이름으로 변경하셔도 됩니다.







사용자 삽입 이미지
다음은 MyView에 그리기 명려을 처리할 메소드를 추가합니다. 좌측과 같이 MyView를 우클릭 하여 Add Action to MyView를 선택 합니다.




 
사용자 삽입 이미지
좌측과 같이 기본으로 만들어진 myAction을 drawItem으로 변경합니다.







사용자 삽입 이미지
이제 소스파일을 생성하기 위해 MyView 클래스를 다시 우클릭하여 Create Files for MyView를 선택합니다.

[Choose] 버튼을 클릭하여 MyView.h와 MyView.m 파일을 생성합니다.



2) NSView 속성 설정
 
이제는 NSView와 버튼들을 배치하고 속성들을 설정해 보겠습니다. 아래와 같이 윈도우를 열고 팔레트에서 여섯번째 메뉴인 Containers를 선택하고, 윈도우로 CustomView를 드래그 하여 가지고 옵니다.
사용자 삽입 이미지

사용자 삽입 이미지
윈도우에서 CustomView가 선택된 상태에서 [shift + command + i]를 눌러 인스펙터를 오픈한 후, 상단에서 Custom Class를 선택합니다.

또는 [command + 5]를 눌러서 바로 Custom Class를 오픈해도 됩니다.

Class 항목에서 이전에 만들어 놓은 MyView를 선택합니다.


사용자 삽입 이미지
완료되면 CustomView가 MyView로 변경됩니다. MyView의 사이즈를 좌측과 같이 적절하게 조절 합니다.

상단의 Window에서 SimpleDraw로의 변경은 이전 장에서 해본 것과 같이 윈도우의 인스펙터를 불러내어 Window Title 항목을 변경합니다.






3) 버튼 생성 및 속성 설정

이제 버튼을 배치합니다. 아래와 같이 팔레트에서 버튼을 불러와 버튼을 복사/붙여넣기 하여 네개의 버튼을 만든 후 MyView 하단에 배치 합니다.
사용자 삽입 이미지

사용자 삽입 이미지
좌측과 같이 네개의 버튼의 제목을 각각 사각형, 원, 선, 문자로 변경합니다.

자세히 보면 버튼의 모양이 기본 버튼과 다른데, 이는 아래에서 설명하겠습니다.









사용자 삽입 이미지
사각형 버튼에서 인스펙터를 열어 보면, Tag에 1로 되어 있습니다. 이는 각각의 버튼이 클릭되었을 때, MyView의  DrawItem 메소드에서 구별하기 위해 설정 합니다.

사각형은 1, 원은 2, 선은 3, 문자는 4로 Tag 값을 각각 설정 합니다.

위의 버튼 모양은 Type 항목에서 Round Textured Button을 선택한 모양 입니다.


이제 버튼 클릭시 동작을 연결합니다. 이전 장과 같이 사각형 버튼을 control키를 누른 상태에서 드래그 하여 MyView에 가져다 놓습니다. 아래와 같이 인스펙터 창이 나타나면 Target/Actiond에서 drawItem을 선택하고 하단의 Connect 버튼을 클릭합니다.
사용자 삽입 이미지
위의 작업을 원, 선, 문자 버튼에서도 똑같이 drawItem에 연결 하여 줍니다.


1.5.3 소스코드 작성

1) 헤더파일 수정

이제 Xcode로 돌아 와서 소스코드를 수정합니다. 우선 MyView.h를 에디터에서 오픈합니다.

#import <Cocoa/Cocoa.h>

@interface MyView : NSView
{
    int itemType;
   
    NSString *txt_message;
    NSMutableDictionary *txt_attributes;
}
- (IBAction)drawItem:(id)sender;
@end

헤더파일에는 아래와 같이 세가지 속성들이 추가되었습니다.

int itemType;
어떤 방식으로 그릴지 설정을 저장할 변수 입니다. 위에서 버튼들의 인스펙터에서 설정 한 tag값(1, 2, 3, 4)들이 저장됩니다.

NSString *txt_message;
출력할 문자를 저장합니다.

NSMutableDictionary *txt_attributes;
폰트, 색상 등 문작의 출력 속성을 저장 합니다.

2) 소스 파일 변경 사항

이제 MyView.m을 에디터에서 오픈합니다. 이번에는 소스가 조금 길어 졌습니다.

#import "MyView.h"

@implementation MyView

- (id)initWithFrame:(NSRect)frameRect
{
    if ((self = [super initWithFrame:frameRect]) != nil) {
        // Add initialization code here
        itemType = 0;
        
        txt_message = [[NSString alloc] initWithString:@"Hello!!!"];
        txt_attributes = [[NSMutableDictionary alloc] init];
        [txt_attributes setObject:[NSColor blueColor]
                                   forKey:NSForegroundColorAttributeName];
    }
    return self;
}

- (void)dealloc {
    [txt_message release];
    [txt_attributes release];
    [super dealloc];
}

- (void)drawRect:(NSRect)rect
{
    const int ITEM_WIDTH = 50;
    const int ITEM_HEIGHT = 50;
   
    NSRect itemRect;
    NSBezierPath *path;
        
    itemRect.origin.x = (rect.size.width/2) - (ITEM_WIDTH/2);
    itemRect.origin.y = (rect.size.height/2) - (ITEM_HEIGHT/2);
   
    itemRect.size.width = ITEM_WIDTH;
    itemRect.size.height = ITEM_HEIGHT;

    switch (itemType) {
    case 1:
        path = [NSBezierPath bezierPathWithRect:itemRect];
        [path fill];
        break;
    case 2:
        path = [NSBezierPath bezierPathWithOvalInRect:itemRect];
        [path fill];
        break;
    case 3:
        path = [NSBezierPath bezierPath];
        [path moveToPoint:itemRect.origin];
        [path lineToPoint:(NSPoint) { itemRect.origin.x + itemRect.size.width, itemRect.origin.y + itemRect.size.height }];
        [path setLineCapStyle: NSButtLineCapStyle];
        [path setLineWidth: 3];
        [path stroke];
        break;
    case 4:
        [txt_message drawAtPoint:(NSPoint) { 20, 20 } withAttributes:txt_attributes];
        break;
    default:
        break;
    }
}

- (IBAction)drawItem:(id)sender
{
    NSLog(@"%d", [sender tag]);
   
    itemType = [sender tag];        
    [self setNeedsDisplay:YES];
}

@end

3) 오브젝트 생성과 해제

자동으로 생성된 initWithFrame 메소드는 이름에서 짐작하듯이 NSView가 처음 초기화 될 때 자동으로 호출됩니다. 주석 설명(// Add initialization code here)과 같이 초기화할 내용들을 아래와 같이 입력합니다.

itemType = 0;

초기에 아무것도 그려지지 않도록 0으로 넣습니다.
      
txt_message = [[NSString alloc] initWithString:@"Hello!!!"];
출력할 텍스트 오브젝트를 생성하고 "Hello!!!"란 문구로 초기화 합니다.

txt_attributes = [[NSMutableDictionary alloc] init];
[txt_attributes setObject:[NSColor blueColor]
                            forKey:NSForegroundColorAttributeName];
텍스트 속성 오브젝트를 생성하고, 폰트칼라만 파란색([NSColor blueColor])으로 설정합니다.

- (void)dealloc {
    [txt_message release];
    [txt_attributes release];
    [super dealloc];
}
dealloc은 오브젝트 사용이 끝나고 메모리에서 해제될 때 호출됩니다. alloc으로 메모리를 할당한 오브젝트들(txt_message, txt_attributes)을, release로 메모리를 해제해 줍니다.

4) 그리기

drawRect는 NSView가 그려져야 할 때, 자동으로 호출됩니다. 그리는 작업을 이곳에서 하면 됩니다.

const int ITEM_WIDTH = 50;
const int ITEM_HEIGHT = 50;
그려질 도형들의 크기를 설정합니다.
  

NSRect itemRect;
NSBezierPath *path;
        
itemRect.origin.x = (rect.size.width/2) - (ITEM_WIDTH/2);
itemRect.origin.y = (rect.size.height/2) - (ITEM_HEIGHT/2);
itemRect.size.width = ITEM_WIDTH;
itemRect.size.height = ITEM_HEIGHT;
그려질 도형들의 위치와 크기를 설정합니다. 인자로 넘어온 rect는 그려져야 할 영역인데 NSView의 크기로 보시면 됩니다. NSView의 중앙에 오도록 계산하여 x, y를 설정 합니다.
 

switch (itemType) {
case 1:
    path = [NSBezierPath bezierPathWithRect:itemRect];
    [path fill];
    break;
case 2:
    path = [NSBezierPath bezierPathWithOvalInRect:itemRect];
    [path fill];
    break;
case 3:
    path = [NSBezierPath bezierPath];
    [path moveToPoint:itemRect.origin];
    [path lineToPoint:(NSPoint) { itemRect.origin.x + itemRect.size.width, itemRect.origin.y + itemRect.size.height }];
    [path setLineCapStyle: NSButtLineCapStyle];
    [path setLineWidth: 3];
    [path stroke];
    break;
case 4:
    [txt_message drawAtPoint:(NSPoint) { 20, 20 } withAttributes:txt_attributes];
    break;
default:
    break;
} 
itemType에 따라 NSBezierPath를 사용하여 사각형, 원, 직선과 문자를 출력합니다. 상세한 설명은  튜토리알 이 후에 다시 하겠습니다. 용도는 오브젝트, 변수, 메소드 명을 보시면 대충 짐작하실 수 있습니다.

다만 [txt_message drawAtPoint:(NSPoint) { 20, 20 } withAttributes:txt_attributes];
를 보시면 x, y 20, 20으로 출력하는데 생각과는 달리 좌측 상단에 나오지 않고 좌측 하단에 출력 됩니다. 이는 기본 좌표 체계가 좌측 하단을 기준으로 시작하기 때문입니다.

5) 사용자 입력 처리

- (IBAction)drawItem:(id)sender
{
    NSLog(@"%d", [sender tag]);
   
    itemType = [sender tag];        
    [self setNeedsDisplay:YES];
}
버튼이 클릭될 때, 호출되는 메소드 입니다. 네개의 버튼이 연결되어 잇지만 [sender tag]로 현재 어느 버튼이 클릭되었는지 알 수 있습니다.

[self setNeedsDisplay:YES];
NSView가 다시 그려져야 됨을 알립니다. drawRect가 호출됩니다.

6) 마무리

이제 빌드 후 실행하고 테스트 해 봅니다. 오류가 나거나 정상적으로 동작하지 않으면 위의 내용을 확인해 보시기 바랍니다.

완전한 이해를 위해서는 Object-c의 문법과 메소드 기타 상세한 설명이 필요 하지만, 단순히 따라 해보는 튜토리알에 의미를 두었습니다. 추후 튜토리알과 다른 항목에서 설명하도록 할려고 합니다.

또한 결정적인 이유는 송구스럽게도 저도 대충 이해하고 구현에 중점을 두고 해보기 때문입니다.