[C++Builder] Component Editor 가 추가된 TMyLabel

볼랜드포럼에 올렸던 글을 블로그에 재게시 합니다.
http://cbuilder.borlandforum.com/impboard/impboard.dll?action=read&db=bcb_tutorial&no=128


VCL 컴포넌트중에서 어떤 것은 폼에 drop 시켜 놓고 컴포넌트에 더블 클릭을 하면 대화창이 나오는 경우가 있습니다.
사용자 대화식으로 데이터를 입출력하는 것입니다. 대표적인 것이 TStatusBar 입니다. Panels 를 사용자 대화창으로 입력하고 삭제하는 것이 가능합니다.

이러한 에디터를 Component Editor 라고 부릅니다.




Component Editor 는 2가지 방법을 통해서 구현할 수 있습니다.
(1) 컴포넌트를 더블 클릭했을 때 에디터를 열어서 작업
(2) 컴포넌트 위에서 마우스 오른쪽 버튼을 클릭했을 때 나타나는 메뉴를 선택하여 작업

만약, Component Editor 를 별도로 지정하지 않았다면 Default Component Editor 가 지정됩니다. 이것은
Event 탭에서 제일 첫번째 이벤트 핸들러를 연결해 주는 것입니다.

Component Editor 를 만들기 위해서는 TComponentEditor 로부터 상속받아서 함수들을 오버라이딩 해야 합니다.
TComponentEditor를 사용하기 위해서는 DesignEditors.hpp 를 include 해야 합니다.
잠깐!
이전 강좌에서는 소스파일 한개에 구현했으나 지금부터는 Run Time 과 Design Time 을 구별하여 작성하도록 하겠습니다. 이전 강좌에서 사용했던 MyLabel.cpp 에는 Lines 속성을 추가한 상태로 사용합니다. Component Editor 는 별도의 파일인 MyLabel_Dsgn.cpp 에 작성합니다. 그리고 컴포넌트를 컴포넌트 팔레트에 등록하기 위한 register 함수를 MyLabel_reg.cpp 에 저장합니다. 그러므로 총 3개의 파일이 만들어집니다.

MyLabel.cpp
MyLabel_Dsgn.cpp
MyLabel_reg.cpp

Component Editor 는 Design Time 용이므로 DesignEditors.hpp 는 MyLabel_Dsgn.cpp 에 추가합니다.

#include  <DesignEditors.hpp> //Component Editor 사용을 위해 포함


TMyLabel 의 Component Editor 를 TMyLabelEditor 라고 하고 TComponentEditor 의 Edit 함수를 오버라이딩합니다.
Edit 함수는 컴포넌트를 더블 클릭했을 때 Component Editor 를 실행시켜줍니다.
class PACKAGE TMyLabelEditor : public TComponentEditor
{
  public:
    void __fastcall Edit(void);
};


기본적인 아이디어는 더블 클릭했을 때 폰트 대화상자를 생성하여 실행되어서 폰트를 변경하고 이것을 TMyLabel 컴포넌트의 Font 속성에 적용하는 것입니다. 그리고 생성한 폰트 대화상자를 파괴하고 함수를 끝냅니다.
void __fastcall TMyLabelEditor::Edit(void)
{
  TFontDialog* FontDlg = new TFontDialog(NULL);  //폰트 대화상자 생성
  FontDlg->Execute();                                       //폰트 대화상자 실행
  ((TMyLabel*)Component)->Font = FontDlg->Font;  //현재 선택된 컴포넌트의 폰트를 대화상자에서 선택한 폰트로 변경
  delete FontDlg;                                               //폰트 대화상자 파괴
}


위에서 Component 는 TComponentEditor 의 property 입니다. 아래와 같이 정의됩니다. 

__property Classes::TComponent* Component = {read=FComponent};


Component Editor 는 연결된 컴포넌트 위에서 더블 클릭했을 때 자동으로 생성되며 FComponent 에 해당 컴포넌트가
어떤 것인지 알려 줍니다. Component property 는 이것을 읽는 방법을 제공합니다. (TMyLabel*)Component 는 Component 를 TMyLabel* 타입으로 타입 캐스팅합니다. 그러면 TMyLabel 의 멤버에 접근할 수 있습니다.
살짝 의문 - 상속 관계에서 부모 타입의 포인터는 부모의 멤버와 , 부모의 멤버를 상속받은 자식의 멤버를 접근할 수 있습니다. virtual 로 하면 자식이 재정의했을 때 재정의한 자식의 멤버를 접근할 수 있습니다. 그럼, 부모는 가지지 않고 자식이 새로 만든 멤버를 부모 타입의 포인터로 접근하려면 어떻게 해야 할까요? 그것은 자식의 타입으로 타입캐스팅하는 것입니다. Component 는 TComponent* 타입입니다. Font는 TComponent 에는 없으며 후손중에서 추가된 것입니다. TComponent* 타입의 Component로는 TComponent 의 멤버또는 TComponent의 멤버를 후손이 재정의했을때 접근가능합니다. Font 는 후손이 새로 만들었으므로 후손 class 의 타입으로 타입 캐스팅합니다.  컴포넌트를 폼에 drop 하면 MyLabel1 객체가 생깁니다. Component 가 TMyLabel 의 객체인 MyLabel1 을 가리키고 있지만 Component 의 타입이 TComponent* 이므로 MyLabel1 의 Font 에 접근할 수 없습니다. 따라서 Font 에 접근하려면 (TMyLabel*)Component 와 같이 타입 캐스팅합니다.


Component Editor 의 class 를 설계하고 구현도 했으니 이제 등록할 차례입니다. 등록은 Register 함수에서 합니다. Register 함수는 MyLabel_reg.cpp 에 저장합니다.
namespace Mylabel_reg
{
        void __fastcall PACKAGE Register()
        {
                 TComponentClass classes[1] = {__classid(TMyLabel)};
                 RegisterComponents("Samples", classes, 0);
                 RegisterComponentEditor(__classid(TMyLabel), __classid(TMyLabelEditor));
        }
}



RegisterComponentEditor 라는 함수를 사용하여 등록했습니다. 첫번째 인수로 연결할 컴포넌트인 TMyLabel을 지정하고 두번째 인수로 TMyLabel 과 연결되는 Component Editor 인 TMyLabelEditor 를 지정했습니다.

살짝 트릭!
RegisterComponentEditor 함수에 TMyLabel 뿐만 아니라 다른 컴포넌트를 사용해도 됩니다. TMyLabelEditor 는 해당
컴포넌트의 Font , Color 를 바꿉니다. 따라서 Font , Color 를 속성으로 가지는 컴포넌트라면 Component Editor 를
TMyLabelEditor 로 바꿀수 있습니다. 다음과 같이 한줄을 추가하면 CheckBox 의 Component Editor 가 바뀝니다.

RegisterComponentEditor(__classid(TCheckBox), __classid(TMyLabelEditor));
설치해보고 CheckBox 를 폼에 drop 한후 더블 클릭해보세요. 폰트 대화상자가 뜹니다.

  설치하고 실행해 봅시다.그런데 한가지 단점이 있습니다. TMyLabel 의 변화를 Component Editor 가 알아채지 못합니다. Component Editor 에서 변경시킨 값은 TMyLabel 에 반영되지만, TMyLabel 의 값은 Component Editor 에 반영되지 않습니다. 때문에, 다시 더블 클릭하면 기본상태로 돌아가고 그 상태에서 OK 버튼을 누르면 TMyLabel 에 반영되어
기본 폰트 상태가 됩니다. 따라서 이번에는 TMyLabel 의 변화를 폰트 대화상자에 알려주는 기능을 추가해 봅시다.

void __fastcall TMyLabelEditor::Edit(void)
{
  TMyLabel* mylabel = (TMyLabel*)Component;  //mylabel 은 선택된 컴포넌트

  TFontDialog* FontDlg = new TFontDialog(NULL); //폰트 대화상자 생성
  FontDlg->Font = mylabel->Font;  //폰트 대화상자에 선택된 컴포넌트의 폰트를 알려준다.
  FontDlg->Execute();                 //폰트 대화상자 실행
  mylabel->Font = FontDlg->Font; //대화상자에서 선택한 폰트를 선택된 컴포넌트에 적용한다.
  Designer->Modified();  //선택된 컴포넌트가 변했음을 알려준다.
  delete FontDlg;         //폰트 대화상자 파괴
}


자 , 이제 TMyLabel 컴포넌트를 더블 클릭하면 폰트를 변경시킬수 있는 대화상자가 열리고 폰트를 변경하면
TMyLabel 에 적용됩니다. 폰트가 변화된 상태에서 다시 더블 클릭하면 폰트 대화상자에는 변경된 폰트가 적용됩니다.
이제 원하는 기능이 구현되었습니다. Designer->Modified(); 는 IDesigner 값을 변화시킵니다. 이렇게 하면 Object Inspector 의 properties 에 변화가 적용됩니다. 해당 부분을 지워보고 어떤 차이가 있는지 알아보는 것도 재밌을 것입니다.

그런데, 이제 욕심이 생기기 시작합니다. 폰트 뿐만이 아니라 Color 도 바꾸고 싶습니다.
그래서 마우스 오른쪽 버튼을 누르면 메뉴가 나타나서 직접 바꿀수 있도록 하고 싶습니다. 이렇게 하려면 Edit 함수가 아닌 다른 방법을 사용해야 합니다. TComponentEditor 의 다음 3가지 함수를 오버라이딩 합니다.

(1)  int __fastcall GetVerbCount(void);
(2)  AnsiString __fastcall GetVerb(int Index);
(3)  void __fastcall ExecuteVerb(int Index);

GetVerbCount 는 메뉴의 개수를 반환합니다.
GetVerb 는 Index 에 따라서 나타나는 메뉴의 이름을 나타냅니다.
ExecuteVerb 는 메뉴를 선택했을 때 실행할 명령을 나타냅니다.

ExecuteVerb 는 컴포넌트를 더블 클릭했을 때 제일 첫번째 메뉴를 실행합니다.
나머지는 마우스 오른쪽 버튼을 눌러서 선택해야 합니다.

자, 이제 아이디어를 생각해 봅시다.

메뉴의 개수는 2개로 합니다. 첫번째는 폰트 대화상자, 두번째는 컬러 대화상자를 실행해서 색을 바꿉니다.
메뉴에 표시할 제목은 Change Font , Change Color 순으로 합니다.
Change Font 를 선택하면 폰트 대화상자가 나타나서 폰트를 바꿀수 있도록 합니다. 이때 현재 Label 의 상태를 읽어서
폰트 대화상자에 반영되도록 합니다. 그리고 OK 버튼을 누르면 폰트 대화상자에서 선택한 폰트가 Label 에 반영되도록 합니다. Change Color 또한 동일합니다.

GetVerbCount , GetVerb , ExecuteVerb 는 다음과 같이 구현합니다.

int __fastcall TMyLabelEditor::GetVerbCount()
{
  return 2;  //선택할 수 있는 Context Menu 의 개수는 2개
}

AnsiString __fastcall TMyLabelEditor::GetVerb(int Index)
{
  AnsiString S;
  switch (Index)
  {
    case 0 : S = "Change Font..."; break;  //첫번째 메뉴
    case 1 : S = "Change Color..."; break; //두번째 메뉴
  }
  return S;
}

void __fastcall TMyLabelEditor::ExecuteVerb(int Index)
{
  switch (Index)
  {
    case 0 : ChangeFont(); break;  //첫번째 메뉴 선택하면 ChangeFont 실행
    case 1 : ChangeColor(); break; //두번째 메뉴 선택하면 ChangeColor 실행
  }
}

잠깐!
ExcuteVerb 가 아니라 ExecuteVerb 입니다. 엉뚱하게 뉴스그룹만 뒤졌네요. 오타가 났다는 생각은 못하고...
그리고 빌더 도움말에 있는 예제는 잘못되었습니다. 함수의 선언에서는 virtual 을 쓸 수 있지만 구현부에서는 쓰면 안됩니다. 그리고.. 난데없이 end; 가 등장하는 일이... 도움말을 급하게 만들면서 델파이 소스위에 그대로 덮어쓰기 한것 같습니다.



Change Font , Change Color 는 다음과 같이 구현합니다.
void __fastcall TMyLabelEditor::ChangeFont()
{
  TMyLabel* mylabel = (TMyLabel*)Component;  //mylabel 은 선택된 컴포넌트

  TFontDialog* FontDlg = new TFontDialog(NULL); //폰트 대화상자 생성
  FontDlg->Font = mylabel->Font;  //폰트 대화상자에 선택된 컴포넌트의 폰트를 알려준다.
  FontDlg->Execute();                 //폰트 대화상자 실행
  mylabel->Font = FontDlg->Font; //대화상자에서 선택한 폰트를 선택된 컴포넌트에 적용한다.
  Designer->Modified();  //선택된 컴포넌트가 변했음을 알려준다.
  delete FontDlg;         //폰트 대화상자 파괴
}

void __fastcall TMyLabelEditor::ChangeColor()
{
  TMyLabel* mylabel = (TMyLabel*)Component;

  TColorDialog* ColorDlg = new TColorDialog(NULL);
  ColorDlg->Color = mylabel->Color;
  ColorDlg->Execute();
  mylabel->Color = ColorDlg->Color;
  Designer->Modified();
  delete ColorDlg;
}



필요한 5개의 함수를 모두 구현했으므로 이제 컴파일합니다.
다음과 같이 bpk 에 MyLabel.cpp , MyLabel_Dsgn.cpp , MyLabel_reg.cpp 를 Add 시켜줍니다.



다음과 같이 되는 것을 알 수 있습니다.