기술탐구

[오픈캡쳐] 개선된 선택영역 지정방법

appleii 2009. 7. 16. 14:33

오픈캡쳐에서 선택영역 지정시에 이미지 범위를 벗어나는 것을 막는 것은 이미 수정되었다. 그러나 여전히 불만족 스럽다. 마우스 드래그를 통해서 범위를 지정할 때 점선 모양의 사각형이 일부만 나온다. 또, 이미 지정된 선택영역 (RubberbandLayer) 이 이미지 밖으로 나갈 수가 있다. 이것을 막는 방법을 연구해 볼 필요가 있다.

(1) 점선 모양 사각형이 전부 나오도록 하는 방법


점선 모양 사각형이 일부만 나오는 이유는 뭘까. 그것은 사각형을 그릴 때 처음 마우스를 누른 (MouseDown) 지점부터 사각형의 시작점이 설정되기 때문이다. 이것을 수정하여 이미지 범위 밖이면 경계선으로 제한해 버리는 함수를 만들 필요가 있다.

uMDI.pas 에 다음과 같은 procedure 를 추가한다. 이름은 SetLimitSelectBounds 이다.


procedure TfrmMDI.SetLimitSelectBounds(var ABeginPt, AEndPt: TPoint);
begin
  //선택영역이 이미지 바깥으로 나가지 못하도록 한계를 정한다.
  NormalizePoint(ABeginPt, AEndPt);
  if ABeginPt.X < 0 then ABeginPt.X := 0;
  if ABeginPt.Y < 0 then ABeginPt.Y := 0;
  if AEndPt.X > FAImage.Bitmap.Width then AEndPt.X := FAImage.Bitmap.Width;
  if AEndPt.Y > FAImage.Bitmap.Height then AEndPt.Y := FAImage.Bitmap.Height;
end; 



좌표값을 넘겨받아서 직접 수정해야 하므로 주소를 직접 넘겨받는 var 키워드를 사용한다. 인수(Argument)로 넘겨받은 ABeginPt, AEndPt 에는 인수(Argument) 임을 나타내는 A가 붙어있다. Delphi 특유의 변수 표기법이다.

기능은 이전에 사용했던 것과 마찬가지로 NormalizePoint 로 좌상단, 우하단의 위치를 정하고 그 값이 실제 이미지의 밖에 있는지를 검사한후, 밖이면 경계값으로 제한하는 것이다. 이전에는 MouseUp 일 때만 경계를 정하지만 별도의 루틴을 만들어서 MouseMove 일 때에도 값을 정해서 점선 모양의 사각형이 Drag 중에도 전부 나타나도록 수정했다.

(2) RubberbandLayer 가 이미지 밖으로 나가는 것을 막는 방법


Drag 가 끝난후에는 점선 모양의 사각형 대신, 새롭게 RubberbandLayer 가 나타난다. 이것은 이름 그대로 고무줄 모양의 Layer 이다. 8개 방향의 Handle 을 잡고 늘리면 쭉~ 늘어난다.

그런데 여기서도 실제 이미지의 경계선 밖으로 나갈 수 있는 문제점이 있다. Handle 을 잡고 당겨도 밖으로는 나가지 못하도록 제한할 필요가 있다. 

TRubberbandLayer 는 Graphics32 에서 사용하는 Class 이다. TRubberbandLayer 는 이벤트가 두 가지 있다. OnResizing, OnUserChange 이다. 이 중에서 OnResizing 이벤트가 크기 조절과 관련된 이벤트이다. OpenCapture 에서는 RubberbandLayer 의 OnResizing 이벤트에 대해서 RBResizing 이라는 이벤트 핸들러가 있다. 동적으로 생성시키는 개체이므로 이벤트와 이벤트 핸들러를 연결시켜 주어야 한다.  OnResizing := RBResizing; 부분을 주목해 보자.


FRBLayer := TRubberbandLayer.Create(FAImage.Layers);

  with FRBLayer do
  begin
    MinWidth := 1;
    MinHeight := 1;
    ChildLayer := FBitmapLayer;
    OnResizing := RBResizing;
    OnMouseDown := RBMouseDown;
  end;


TRubberbandLayer 타입의 개체인 FRBLayer 를 생성하고

FRBLayer := TRubberbandLayer.Create(FAImage.Layers);

OnResizing 이벤트를 처리할 이벤트 핸들러로 RBResizing 을 지정한다.

FRBLayer.OnResizing := RBResing;

RBResizing 의 선언부는 아래와 같다.

procedure RBResizing(Sender: TObject; const OldLocation: TFloatRect; var NewLocation: TFloatRect; DragState: TDragState; Shift: TShiftState);

OldLocation 과 NewLocation 을 주목해 보자.

OldLocation 은 이름 그대로 RubberbandLayer 의 Handle 을 잡고 Drag 하기 전 위치이다. NewLocation 은 Drag 해서 이동시킨 위치이다. 위치가 TFloatRect 형태가 되어서 정수로 바꾸어주기 위해서 반올림해주는 Round 를 사용한다. 예를 들면 왼쪽 Handle 을 잡고 Drag 시켰을 때

dsSizeL: NewLocation.Left := Round(NewLocation.Left);

좌상단(Top, Left)일 경우는 두가지를 다 해줘야 한다.

dsSizeTL:
  begin
    NewLocation.Top := Round(NewLocation.Top);
    NewLocation.Left := Round(NewLocation.Left);
  end;


DragState 는 RubberbandLayer 를 이동시키는 상태(dsMove)인지 Handle 을 잡고 당기는 것인지를 나타내는 것이다. 8개 방향의 Handle 과 이동상태(dsMove) 아무것도 안하는 상태(dsNone) 포함 모두 10가지 상태가 있다.

type TDragState = (dsNone, dsMove, dsSizeL, dsSizeT, dsSizeR, dsSizeB, dsSizeTL, dsSizeTR, dsSizeBL, dsSizeBR);

오픈캡쳐는 기본적으로 9가지 DragState 에 대한 처리를 하고 있다. 특이한 것은 dsMove 일 때의 오른쪽(Right)과 바닥쪽(Bottom)에 대한 처리이다.

dsMove:
begin
  NewLocation.Left := Round(NewLocation.Left);
  NewLocation.Top := Round(NewLocation.Top);
  NewLocation.Right := Round(NewLocation.Left + (OldLocation.Right -
    OldLocation.Left));
  NewLocation.Bottom := Round(NewLocation.Top + (OldLocation.Bottom -
    OldLocation.Top));
end;

 
오른쪽 위치를 NewLocation.Left 에 RubberbandLayer 의 폭(Width)을 더한 위치로 정하고 있다. 마찬가지로 바닥쪽 위치는 NewLocation.Top 에 높이(Height)를 더한 위치이다.

그럼, 이제 경계점(Top, Left, Bottom, Right) 밖으로 나가지 못하도록 만들어 보자. NewLocation 의 네 점을 고정시키면 된다. 이 때 주의할 것은 이동중(dsMove) 일 때는 별도의 처리가 필요하다는 점이다. dsMove 일 때의 처리를 해주지 않으면 RubberbandLayer 가 찌그러져 버린다.


    //RubberbandLayer 가 이미지 밖으로 나가는 것을 막는다.
    if NewLocation.Left < 0 then
    begin
      NewLocation.Left := 0;
      if DragState = dsMove then
      NewLocation.Right := Round(NewLocation.Left + (OldLocation.Right -
      OldLocation.Left));
    end;

    if NewLocation.Top < 0 then
    begin
      NewLocation.Top := 0;
      if DragState = dsMove then
      NewLocation.Bottom := Round(NewLocation.Top + (OldLocation.Bottom -
      OldLocation.Top));
    end;

    if NewLocation.Right > FAImage.Bitmap.Width then
    begin
      NewLocation.Right := FAImage.Bitmap.Width;
      if DragState = dsMove then
      NewLocation.Left := Round(NewLocation.Right + (OldLocation.Left -
      OldLocation.Right));
    end;

    if NewLocation.Bottom > FAImage.Bitmap.Height then
    begin
      NewLocation.Bottom := FAImage.Bitmap.Height;
      if DragState = dsMove then
      NewLocation.Top := Round(NewLocation.Bottom + (OldLocation.Top - 
      OldLocation.Bottom));
    end;

3) RubberbandLayer 위에서 더블 클릭후 Crop 기능

RubberbandLayer 위에서의 더블 클릭 동작은 OnMouseDown 이벤트를 사용해야 한다. OnMouseDown 이벤트 핸들러로 RBMouseDown 을 지정한다.

FRBLayer.OnMouseDown := RBMouseDown;

마우스의 클릭은 TShiftState 형 변수인 Shift 로 넘어온다. 더블 클릭은 ssDouble 이다. Shift 는 키보드의 기능키와 조합이 가능하므로 집합형으로 정의되어 있다. Shift 중에서 ssDouble 이 있는지 확인하면 된다.

if ssDouble in Shift then
begin
  ...
end;

Crop 이 tsSelect 일 때만 동작하므로 조건문을 한개 더 넣어주면 다음과 같다.


procedure TfrmMDI.RBMouseDown(Sender: TObject; Button: TMouseButton; Shift: TShiftState; X, Y: Integer);
begin
  if ssDouble in Shift then
  begin
    // tsSelect 일때 RubberbandLayer 위에서 더블 클릭하면 현재 창에서 Crop 된다.
    if ToolSel = tsSelect then
    begin
      SaveUndoImage(FAImage.Bitmap);
      FRBLayer.ChildLayer := nil;
      FRBLayer := nil;

      with FAImage do
      begin
        Bitmap.Assign(FBitmapLayer.Bitmap);
        Layers.Clear;
        Cursor := 12;
      end;

      with frmMain do
      begin
        pnlBitmapLayer.Visible := False;
        UpdateMenu(nil);
      end;
      FormActivate(Sender);
    end;
  end;
end;


기본 과정은 다음과 같다.

(1) SaveUndoImage(FAImage.Bitmap); 을 통해서 현재 이미지를 저장한다.
(2) RubberbandLayer 를 없앤다.
     FRBLayer.ChildLayer := nil;
     FRBLayer := nil;
(3) FAImage 에 현재 선택영역의 비트맵(FBitmapLayer.Bitmap)을 할당한다.
     FAImage.Bitmap.Assign(FBitmapLayer.Bitmap);

수정한 기능들을 플래시 동영상으로 확인해 보면 다음과 같다.

P.S. 이 기능들은 오픈캡쳐의 개발방향과 다르기 때문에 추가되지는 않을 것이다. 그러나 어떤 원리로 동작하는지 약간이나마 이해하는데는 도움이 될 것이다.