잡동사니 시장에
create: 2004/07/17, last update: 2004/08/25

ATL 로 작성한 Active X 컨트롤의 샘플


ATL 을 사용해 , 프롭퍼티 페이지 , 접속 포인트 , thread를 실장한 Active X 컨트롤을 만들 기회가 있었으므로 , 자신의 메모에 샘플을 남겼습니다.
덧붙여 이하의 설명에서는 VC++ 6 을 사용하고 있습니다.
컨트롤의 작성
VC 을 기동해 , 메뉴로부터 [파일] - [신규 작성] 을 선택해 ,[신규 작성] 다이얼로그를 표시합니다. [프로젝트] 탭으로 ,[ATL COM AppWizard] 를 선택해 , 프로젝트명을 적당하게 (여기에서는 atlctrlsample 으로 했던) 입력해 ,[OK] 를 클릭합니다.

신규 작성 다이얼로그

[ATL COM AppWizard 스텝 1/1] 에서 , 서버 타입을 다이나믹 링크 라이브러리 (DLL) 로 해 ,MFC 의 서포트에 체크를 넣고 (MFC 를 사용하지 않는 경우는 , 당연히 체크의 필요는 없습니다),[종료] 를 클릭합니다.

ATL COM AppWizard 스텝 1/1

확인을 위한 [신규 프로젝트 정보] 로 [OK] 를 클릭합니다.

신규 프로젝트 정보

다음에 작성하는 컨트롤로부터 , 접속 포인트를 사용해 이벤트 통지할 때에 사용하는 ATL 오브젝트를 작성합니다.
메뉴로부터 [삽입] - [ATL 오브젝트의 신규 작성] 을 선택해 [ATL 오브젝트 위저드] 를 표시합니다.
[카테고리] 에 오브젝트 ,[오브젝트] 에 심플 오브젝트를 선택해 ,[다음에] 를 클릭합니다.

ATL 오브젝트 위저드

표시되는 [ATL 오브젝트 위저드의 프롭퍼티] 의 [이름] 탭으로 [쇼트 네임] 을 적당하게 (여기에서는 SampleParam 으로 했던) 입력해 ,[애트리뷰트(attribute)] 탭으로 [thread 모델] 을 아파트먼트에 ,[인터페이스] 를 듀얼 , [아그리게이션] 을 네 로 해 ,[OK] 를 클릭합니다.

ATL 오브젝트 위저드의 프롭퍼티 - 이름

ATL 오브젝트 위저드의 프롭퍼티 - 애트리뷰트(attribute)

다음에 컨트롤을 작성합니다.
메뉴로부터 [삽입] - [ATL 오브젝트의 신규 작성] 을 선택해 [ATL 오브젝트 위저드] 를 표시합니다.
[컨트롤] 에 오브젝트 ,[오브젝트] 에 풀 컨트롤을 선택해 ,[다음에] 를 클릭합니다.

ATL 오브젝트 위저드

표시되는 [ATL 오브젝트 위저드의 프롭퍼티] 의 [이름] 탭으로 [쇼트 네임] 을 적당하게 (여기에서는 SampleCtrl 으로 했던) 입력해 ,[애트리뷰트(attribute)] 탭으로 [thread 모델] 을 아파트먼트에 ,[인터페이스] 를 듀얼 , [아그리게이션] 을 네에 ,[ISupportErrorInfo 서포트] 와 [connection 포인트의 서포트] 에 체크를 넣어 [OK] 를 클릭합니다.
※[그 외],[스톡 프롭퍼티] 는 디폴트인 채입니다.

ATL 오브젝트 위저드의 프롭퍼티 - 이름

ATL 오브젝트 위저드의 프롭퍼티 - 애트리뷰트(attribute)

[ClassView] 에 작성된 클래스가 표시되고 있는 것을 확인해 주세요.

ClassView

우선 , 메뉴로부터 [빌드] - [빌드] 를 선택해 , 빌드 해 주세요. 라이브러리에 최신의 것 SDK 을 사용하고 있는 경우 ,
c:\documents and settings\bono\my documents\visual studio projects\atlctrlsample\samplectrl.h(97) : 
error C2668: 'InlineIsEqualGUID' : 뎔艱겡걜僿獨痔� 호출을 해결할 수가 없습니다. 
(신기능 ; 梏璟을 참조)
와 같은 에러가 되는 경우가 있습니다. 원인은 마이크로소프트 서포트 기술 정보 243298 - [VC60] C2668 에러가 InlineIsEqualGUID 의 정의 부분에서 발생 으로 리포트 되고 있습니다.
나의 환경에서도 새로운 SDK 을 참조하고 있기 (위해)때문에 , 이 에러가 나옵니다. 서포트 기술 정보에서는 몇개의 회피 방법이 소개되고 있습니다만 , 나는 아래와 같은 방법을 선택했습니다.

컨트롤의 클래스인 CSampleCtrl 의 헤더 CSampleCtrl.h 의 것 ISupportsErrorInfo 의 실장 부분 InterfaceSupportsErrorInfo() 의 것 InlineIsEqualGUID() 을 호출하고 있는 개소에 ATL 의 InlineIsEqualGUID() 를 사용하도록 , 식별자 ATL:: 를 부가했습니다.
    STDMETHOD(InterfaceSupportsErrorInfo)(REFIID riid)
    {
        static const IID* arr[] = 
        {
            &IID_ISampleCtrl,
        };
        for (int i=0; i<sizeof(arr)/sizeof(arr[0]); i++)
        {
            if (ATL::InlineIsEqualGUID(*arr[i], riid))
                return S_OK;
        }
        return S_FALSE;
    }
편집이 살면(자) , 재차 빌드 해 주세요.
위저드로 컨트롤을 작성했을 때에 , 프로젝트의 디렉토리내에 컨트롤 테스트용의 것 HTML 이 생성되고 있기 때문에 (여기에서는 SampleCtrl.htm) 빌드가 끝나면(자) , 이 테스트 페이지를 IE 로 열려 , 이하와 같이 표시되면 OK 입니다.
※IE 의 설정으로 Active X 컨트롤을 표시할 수 있도록 해 두어 주세요.

IE 위에서 표시한 컨트롤

프롭퍼티를 추가하는
프롭퍼티를 추가합니다. 여기에서는 디폴트로 컨트롤에 표시되고 있는 캐릭터 라인 "ATL 3.0 : SampleCtrl" 을 프롭퍼티로부터 설정할 수 있도록(듯이) 합니다.
최초로 IDL 에의 프롭퍼티의 추가와 대응하는 CSampleCtrl 의 메소드를 추가합니다. [ClassView] 그리고 ISampleCtrl 를 오른쪽 클릭해 , 표시되는 pop-up menu로부터 [프롭퍼티의 추가] 를 선택합니다.

프롭퍼티를 추가

[인터페이스에 프롭퍼티를 추가] 다이얼로그가 표시되기 때문에 ,[프롭퍼티의 종류] 에 BSTR,[프롭퍼티명] 에 DisplayString 를 설정해 ,[OK] 를 클릭합니다.

인터페이스에 프롭퍼티를 추가

다음에 , 프롭퍼티치를 보존하기 위한 변수 멤버를 추가합니다. SampleCtrl.h (을)를 열어 , 클래스 정의에 CString 의 변수 m_DisplayString 를 추가합니다.
            (rc.left + rc.right) / 2, 
            (rc.top + rc.bottom) / 2, 
            pszText, 
            lstrlen(pszText));

        return S_OK;
    }

private:
    // 프롭퍼티 DisplayString 의 값을 보관 유지한다. 
    CString m_DisplayString;
};

#endif //__SAMPLECTRL_H_
다음에 , 프롭퍼티치를 m_DisplayString 로 설정/취득하는 코드를 씁니다.
조금 전 , 프롭퍼티를 추가했을 때에 CSampleCtrl.cpp 에 get_DisplayString(),put_DisplayString() 가 추가되고 있기 때문에 , 여기에 코드를 추가합니다.

추가된 메소드

여기에서는 이하와 같이 편집했습니다.
STDMETHODIMP CSampleCtrl::get_DisplayString(BSTR *pVal)
{
    AFX_MANAGE_STATE(AfxGetStaticModuleState())

    *pVal = m_DisplayString.AllocSysString();

    return S_OK;
}

STDMETHODIMP CSampleCtrl::put_DisplayString(BSTR newVal)
{
    AFX_MANAGE_STATE(AfxGetStaticModuleState())

    m_DisplayString = newVal;
    // 컨트롤이 변경된 것을 설정한다. 
    SetDirty(TRUE);
    // 재묘화 하고 싶은 것을 컨테이너에 통지한다. 
    FireViewChange();

    return S_OK;
}
다음에 m_DisplayString 를 표시하도록(듯이) 디폴트의 묘화 코드를 변경합니다.
CSampleCtrl.h 의 OnDraw() 를 이하와 같이 편집했습니다.
    HRESULT OnDraw(ATL_DRAWINFO& di)
    {
        RECT& rc = *(RECT*)di.prcBounds;
        Rectangle(di.hdcDraw, rc.left, rc.top, rc.right, rc.bottom);

        SetTextAlign(di.hdcDraw, TA_CENTER|TA_BASELINE);
        // 프롭퍼티치를 표시하도록(듯이) 변경한다. 
        //LPCTSTR pszText = _T("ATL 3.0 : SampleCtrl");
        LPCTSTR pszText = m_DisplayString;
        TextOut(di.hdcDraw, 
            (rc.left + rc.right) / 2, 
            (rc.top + rc.bottom) / 2, 
            pszText, 
            lstrlen(pszText));

        return S_OK;
    }
이것으로 , 프롭퍼티를 표시할 수 있게 됩니다. 라고 좋은 싶은 곳입니다만 , 아직입니다. IE 등에서 프롭퍼티를 설정하고 싶은 경우 ,IPersistPropertyBag 를 실장할 필요가 있습니다. 그렇다고 해도 ,ATL 그럼 IPersistPropertyBagImpl 이라고 하는 디폴트의 실장이 제공되고 있기 때문에 , 자신의 컨트롤 클래스에서 이것을 계승하는 것만으로 끝납니다.
CSampleCtrl.h 의 클래스 정의의 계승하는 클래스를 기술하는 부분에
public IPersistPropertyBagImpl<CSampleCtrl> // 프롭퍼티를 취급하는
을 추가합니다.
class ATL_NO_VTABLE CSampleCtrl : 
    public CComObjectRootEx<CComSingleThreadModel>,
    public IDispatchImpl<ISampleCtrl, &IID_ISampleCtrl, &LIBID_ATLCTRLSAMPLELib>,
    public CComControl<CSampleCtrl>,
    public IPersistStreamInitImpl<CSampleCtrl>,
    public IOleControlImpl<CSampleCtrl>,
    public IOleObjectImpl<CSampleCtrl>,
    public IOleInPlaceActiveObjectImpl<CSampleCtrl>,
    public IViewObjectExImpl<CSampleCtrl>,
    public IOleInPlaceObjectWindowlessImpl<CSampleCtrl>,
    public ISupportErrorInfo,
    public IConnectionPointContainerImpl<CSampleCtrl>,
    public IPersistStorageImpl<CSampleCtrl>,
    public ISpecifyPropertyPagesImpl<CSampleCtrl>,
    public IQuickActivateImpl<CSampleCtrl>,
    public IDataObjectImpl<CSampleCtrl>,
    public IProvideClassInfo2Impl<&CLSID_SampleCtrl, &DIID__ISampleCtrlEvents, &LIBID_ATLCTRLSAMPLELib>,
    public IPropertyNotifySinkCP<CSampleCtrl>,
    public CComCoClass<CSampleCtrl, &CLSID_SampleCtrl>,
    public IPersistPropertyBagImpl<CSampleCtrl>    // 프롭퍼티를 취급하는
{
public:
    CSampleCtrl()
    {
게다가 클래스 정의중의 것 BEGIN_COM_MAP(CSampleCtrl) 으로부터 END_COM_MAP() 의 사이에
COM_INTERFACE_ENTRY(IPersistPropertyBag)
를 추가합니다.
    COM_INTERFACE_ENTRY(IConnectionPointContainer)
    COM_INTERFACE_ENTRY(ISpecifyPropertyPages)
    COM_INTERFACE_ENTRY(IQuickActivate)
    COM_INTERFACE_ENTRY(IPersistStorage)
    COM_INTERFACE_ENTRY(IDataObject)
    COM_INTERFACE_ENTRY(IProvideClassInfo)
    COM_INTERFACE_ENTRY(IProvideClassInfo2)
    COM_INTERFACE_ENTRY(IPersistPropertyBag)
END_COM_MAP()
마지막으로 , 프롭퍼티의 명칭을 정의하기 위해(때문에) ,BEGIN_PROP_MAP(CSampleCtrl) 로부터 END_PROP_MAP() 의 사이에
PROP_ENTRY("DisplayString", 1, GetObjectCLSID())
를 추가합니다. 덧붙여 제 2 인수의 것 1 은 , 위저드에 의해 IDL 에 추가된 프롭퍼티 DisplayString 의 프롭퍼티 ID 입니다.
BEGIN_PROP_MAP(CSampleCtrl)
    PROP_DATA_ENTRY("_cx", m_sizeExtent.cx, VT_UI4)
    PROP_DATA_ENTRY("_cy", m_sizeExtent.cy, VT_UI4)
    PROP_ENTRY("DisplayString", 1, GetObjectCLSID())
    // Example entries
    // PROP_ENTRY("Property Description", dispid, clsid)
    // PROP_PAGE(CLSID_StockColorPage)
END_PROP_MAP()
빌드가 끝나면(자) , 프롭퍼티치를 설정하도록(듯이) 컨트롤 테스트용의 것 HTML (SampleCtrl.htm) 을 이하와 같이 편집합니다.
<HTML>
<HEAD>
<TITLE>오브젝트 SampleCtrl 용 ATL 3.0 테스트 페이지</TITLE>
</HEAD>
<BODY>
<OBJECT ID="SampleCtrl" CLASSID="CLSID:B94F2CEA-B317-4CEC-8448-B9E733BE663A">
<PARAM NAME="DisplayString" VALUE="안녕하세요">
</OBJECT>
</BODY>
</HTML>
편집이 끝나면(자) , 테스트 페이지를 IE 로 열려 주세요. 이하와 같이 표시되면 OK 입니다.

IE 그리고 표시한 컨트롤

덧붙여 프롭퍼티를 실장하면(자) , 이하와 같은 다이얼로그가 나오기 시작하게 됩니다.

경고 다이얼로그

이것은 , 컨트롤이 안전이라고 마킹되어 있지 않기 때문입니다. 이것을 마크 하기 위해서 IObjectSafety 를 실장합니다. ATL 에는 IObjectSafetyImpl 이 제공되고 있기 때문에 , 통상은 , 이것을 컨트롤 클래스에서 계승할 뿐입니다.
CSampleCtrl.h 의 클래스 정의의 계승하는 클래스를 기술하는 부분에
public IObjectSafetyImpl<CSampleCtrl, INTERFACESAFE_FOR_UNTRUSTED_CALLER | INTERFACESAFE_FOR_UNTRUSTED_DATA>
를 추가합니다. 여기에서는 , 스크립트의 안전성과 초기화시의 데이터의 안전성을 보증하고 있습니다.
class ATL_NO_VTABLE CSampleCtrl : 
    public CComObjectRootEx<CComSingleThreadModel>,
    public IDispatchImpl<ISampleCtrl, &IID_ISampleCtrl, &LIBID_ATLCTRLSAMPLELib>,
    public CComControl<CSampleCtrl>,
    public IPersistStreamInitImpl<CSampleCtrl>,
    public IOleControlImpl<CSampleCtrl>,
    public IOleObjectImpl<CSampleCtrl>,
    public IOleInPlaceActiveObjectImpl<CSampleCtrl>,
    public IViewObjectExImpl<CSampleCtrl>,
    public IOleInPlaceObjectWindowlessImpl<CSampleCtrl>,
    public ISupportErrorInfo,
    public IConnectionPointContainerImpl<CSampleCtrl>,
    public IPersistStorageImpl<CSampleCtrl>,
    public ISpecifyPropertyPagesImpl<CSampleCtrl>,
    public IQuickActivateImpl<CSampleCtrl>,
    public IDataObjectImpl<CSampleCtrl>,
    public IProvideClassInfo2Impl<&CLSID_SampleCtrl, &DIID__ISampleCtrlEvents, &LIBID_ATLCTRLSAMPLELib>,
    public IPropertyNotifySinkCP<CSampleCtrl>,
    public CComCoClass<CSampleCtrl, &CLSID_SampleCtrl>,
    public IPersistPropertyBagImpl<CSampleCtrl>,    // 프롭퍼티를 취급하는
    public IObjectSafetyImpl<CSampleCtrl, INTERFACESAFE_FOR_UNTRUSTED_CALLER | INTERFACESAFE_FOR_UNTRUSTED_DATA>
{
public:
    CSampleCtrl()
    {
게다가 클래스 정의중의 것 BEGIN_COM_MAP(CSampleCtrl) 으로부터 END_COM_MAP() 의 사이에
COM_INTERFACE_ENTRY(IObjectSafety)
를 추가합니다.
    COM_INTERFACE_ENTRY(IConnectionPointContainer)
    COM_INTERFACE_ENTRY(ISpecifyPropertyPages)
    COM_INTERFACE_ENTRY(IQuickActivate)
    COM_INTERFACE_ENTRY(IPersistStorage)
    COM_INTERFACE_ENTRY(IDataObject)
    COM_INTERFACE_ENTRY(IProvideClassInfo)
    COM_INTERFACE_ENTRY(IProvideClassInfo2)
    COM_INTERFACE_ENTRY(IPersistPropertyBag)
    COM_INTERFACE_ENTRY(IObjectSafety)
END_COM_MAP()
프롭퍼티 페이지의 추가
프롭퍼티 페이지와는 VB 등에서 컨트롤을 이용할 때에 , 프롭퍼티치를 다이얼로그로부터 입력할 수 있도록(듯이) 한 것입니다.
아래는 VB 으로 캘린더 컨트롤의 프롭퍼티 페이지를 표시한 예입니다.

캘린더 컨트롤의 프롭퍼티 페이지

메뉴로부터 [삽입] - [ATL 오브젝트의 신규 작성] 을 선택해 [ATL 오브젝트 위저드] 를 표시합니다.
[카테고리] 에 컨트롤 ,[오브젝트] 에 프롭퍼티 페이지를 선택해 ,[다음에] 를 클릭합니다.

ATL 오브젝트 위저드

표시되는 [ATL 오브젝트 위저드의 프롭퍼티] 의 [이름] 탭으로 [쇼트 네임] 을 적당하게 (여기에서는 SampleCtrlPropPage 으로 했던) 입력해 ,[string] 탭으로 [타이틀] 에 샘플 프롭퍼티 ,[문서의 문자] 에 샘플의 프롭퍼티 페이지 , [헬프 파일] 을 하늘로 설정해 ,[OK] 를 클릭합니다.
※[애트리뷰트(attribute)] 는 디폴트인 채입니다.

ATL 오브젝트 위저드의 프롭퍼티 - 이름

ATL 오브젝트 위저드의 프롭퍼티 - string

프로젝트에 프롭퍼티 페이지용의 다이얼로그와 클래스가 삽입됩니다.
삽입된 다이얼로그를 보겠습니다. [ResourceView] 그리고 [Dialog] 트리아래의 것 IDD_SAMPLECTRLPROPPAGE 을 열어 주세요.
프롭퍼티 페이지용의 다이얼로그가 추가되고 있습니다. 덧붙여서 , 프롭퍼티 페이지에서는 1 개의 다이얼로그가 실행시에 표시되는 프롭퍼티 페이지의 1 개 탭에 상당합니다. 예를 들면 , 위의 캘린더 컨트롤의 프롭퍼티 페이지와 같이 [전반],[폰트],[색] 의 3 살의 탭을 작성하고 싶은 경우는 ,3 개의 프롭퍼티 페이지 (다이얼로그) 를 작성할 필요가 있습니다.

다이알고그 에디터에서의 표시

이것을 편집해 ,DisplayString 용무의 텍스트 박스를 작성합니다.
작성한 텍스트 박스의 것 ID 은 IDC_DISPLAY_STRING 으로 했습니다.

텍스트 박스와 프롭퍼티

다음에 프롭퍼티 DisplayString 의 내용을 다이얼로그로부터 편집할 수 있도록(듯이) 코드를 씁니다.
[ClassView] (으)로부터 CSampleCtrlPropPage 를 오른쪽 클릭해 , 표시되는 pop-up menu로부터 [Windows 메세지 핸들러의 추가] 를 선택합니다.

Windows 메세지 핸들러의 추가

표시된 [CSampleCtrlPropPage 클래스에 신규 윈도우 메세지와 이벤트 핸들러를 추가] 다이얼로그로 , [클래스에서 사용 가능하게 하는 메세지의 필터] 에 다이얼로그 를 선택해 ,[신규 윈도우 메세지/이벤트] 로 WM_INITDIALOG 를 더블 클릭 해 [기존의 메세지/이벤트 핸들러] 에 이동시켜,[OK] 를 클릭합니다.

CSampleCtrlPropPage 클래스에 신규 윈도우 메세지와 이벤트 핸들러를 추가

추가되었다 OnInitDialog() 를 이하와 같이 편집합니다.
    LRESULT OnInitDialog(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled)
    {
        AFX_MANAGE_STATE(AfxGetStaticModuleState());

        if (m_nObjects <= 0) {
            // 당 프롭퍼티 페이지에 관련짓고 되고 있는 오브젝트가 없는 경우 ,
            return 0;
        }

        // 프롭퍼티의 값을 CSampleCtrl 로부터 취득해 ,
        // 다이얼로그상의 컨트롤에 반영한다. 
        CComQIPtr<ISampleCtrl, &IID_ISampleCtrl> pSampleCtrl(m_ppUnk[0]);

        CComBSTR displayString;

        if (SUCCEEDED(pSampleCtrl->get_DisplayString(&displayString.m_str))) {
            SetDlgItemText(IDC_DISPLAY_STRING, CString(displayString));
        }

        // 변경 플래그를 초기화해 둔다. 
        SetDirty(FALSE);

        return 0;
    }
다음에 프롭퍼티 페이지로 [적용] 또는 [OK] 이 클릭되었을 때의 핸들러 Apply() 를 이하와 같이 편집합니다.
    STDMETHOD(Apply)(void)
    {
        AFX_MANAGE_STATE(AfxGetStaticModuleState());
        
        // 다이얼로그 컨트롤의 값을 , 당 프롭퍼티 페이지에
        // 관련지을 수 있고 있는 CSampleCtrl 오브젝트 모두에게 반영한다. 
        BOOL bFailure = FALSE;

        for (UINT i = 0; i < m_nObjects; i++) {

            CComQIPtr<ISampleCtrl, &IID_ISampleCtrl> pSampleCtrl(m_ppUnk[i]);

            CComBSTR displayString;

            GetDlgItemText(IDC_DISPLAY_STRING, displayString.m_str);
            if (FAILED(pSampleCtrl->put_DisplayString(displayString))) {
                bFailure = TRUE;
            }
        }

        if (bFailure) {
            // 실패가 있었을 때는 , 에러 정보를 취득해 표시한다. 
            CComPtr<IErrorInfo> pError;
            CComBSTR strError;
            GetErrorInfo(0, &pError);
            pError->GetDescription(&strError);
            MessageBox(CString(strError), _T("프롭퍼티의 보존에 실패했습니다. "), MB_OK | MB_ICONEXCLAMATION);
            return E_FAIL;
        }

        m_bDirty = FALSE;
        return S_OK;
    }
텍스트 박스가 변경되었을 때에 변경 플래그를 설정하도록(듯이) 코드를 추가합니다.
[ClassView] (으)로부터 CSampleCtrlPropPage 를 오른쪽 클릭해 , 표시되는 pop-up menu로부터 [Windows 메세지 핸들러의 추가] 를 선택합니다.
표시되는 [CSampleCtrlPropPage 클래스에 신규 윈도우 메세지와 이벤트 핸들러를 추가] 다이얼로그로 , [클래스 또는 오브젝트] 로부터 IDC_DISPLAY_STRING 를 선택해 ,[신규 윈도우 메세지/이벤트] 에 텍스트 박스용의 메세지가 표시되면(자) , 그 중에서 EN_CHANGE 를 더블 클릭 합니다.
[멤버 함수의 추가] 다이얼로그가 표시되므로 ,[멤버 함수명] 에 OnChangeDisplayString 라고 입력해 , [OK] 를 클릭합니다.

멤버 함수의 추가

[기존의 메세지/이벤트 핸들러] 에 EN_CHANGE 가 이동하므로 , 그대로 [OK] 를 클릭합니다.

CSampleCtrlPropPage 클래스에 신규 윈도우 메세지와 이벤트 핸들러를 추가

추가되었다 OnChangeDisplayString() 를 이하와 같이 편집합니다.
    LRESULT OnChangeDisplayString(WORD wNotifyCode, WORD wID, HWND hWndCtl, BOOL& bHandled)
    {
        AFX_MANAGE_STATE(AfxGetStaticModuleState());
        SetDirty(TRUE);
        return 0;
    }
CSampleCtrl 의 프롭퍼티와 이 프롭퍼티 페이지를 관련짓기 위해서(때문에) ,CSampleCtrl 의 클래스 정의의 것 BEGIN_PROP_MAP() 으로부터 END_PROP_MAP 의 사이의 것
PROP_ENTRY("DisplayString", 1, GetObjectCLSID())
을 ,
PROP_ENTRY("DisplayString", 1, CLSID_SampleCtrlPropPage)
과 같이 고쳐 씁니다. 고쳐 쓰고가 끝나면(자) 빌드 해 주세요.
빌드가 끝나면(자) , 즉시 컨트롤을 테스트해 보겠습니다. 여기에서는 ,VB 을 사용하고 있습니다.
작성한 컨트롤을 폼에 실어 프롭퍼티 페이지를 표시해 ,DisplayString 의 값을 변경해 , 적용을 클릭해 보세요. 컨트롤로 설정한 캐릭터 라인이 변경되는 것이 확인할 수 있다고 생각합니다.
프롭퍼티 페이지 VB 데자이나에서의 표시
접속 포인트의 추가
최초로 접속 포인트를 사용해 이벤트 통지할 때에 사용하는 ATL 오브젝트를 완성시킵니다.
이벤트 통지로 사용하는 오브젝트란 , 먼저컨트롤의 작성 으로 정의만 되어 있었다 SampleParam 입니다.
SampleParam 의 프롭퍼티로서 이벤트 통지로 통지하고 싶은 내용을 정의합니다. 뭐든지 좋습니다만 , 여기에서는 , 캐릭터 라인을 건네주도록(듯이) 하고 싶다고 생각하기 때문에 , 캐릭터 라인을 보관 유지하는 Msg 프롭퍼티를 추가합니다.
[ClassView] 그리고 ISampleParam 를 오른쪽 클릭해 , 표시되는 pop-up menu로부터 [프롭퍼티의 추가] 를 선택합니다. [인터페이스에 프롭퍼티를 추가] 다이얼로그가 표시되므로 ,[프롭퍼티의 종류] 에 BSTR,[프롭퍼티명에] Msg 설정해 ,[OK] 를 클릭해 주세요.

인터페이스에 프롭퍼티를 추가

CSampleParam 의 클래스 정의에 프롭퍼티 Msg 의 내용을 보관 유지하는 멤버 m_Msg 를 정의합니다.
    // 프롭퍼티 Msg 의 내용을 보관 유지한다. 
    CString m_Msg;
};

#endif //__SAMPLEPARAM_H_
위저드에 의해 추가된 get_Msg,put_Msg 의 내용을 이하와 같이 편집합니다.
STDMETHODIMP CSampleParam::get_Msg(BSTR *pVal)
{
    AFX_MANAGE_STATE(AfxGetStaticModuleState())

    *pVal = m_Msg.AllocSysString();

    return S_OK;
}

STDMETHODIMP CSampleParam::put_Msg(BSTR newVal)
{
    AFX_MANAGE_STATE(AfxGetStaticModuleState())

    m_Msg = newVal;

    return S_OK;
}
이것으로 ,SampleParam 는 완성입니다.
다음에 접속 포인트를 추가합니다. [ClassView] 그리고 _ISampleCtrlEvents 를 오른쪽 클릭해 , 표시되는 pop-up menu로부터 [메소드의 추가] 를 선택합니다.
표시되는 [인터페이스에 메소드를 추가] 로 [메소드명] 에 SampleMethod, 파라미터에 [in] ISampleParam* sampParam 를 설정해 ,[OK] 를 클릭합니다.

인터페이스에 메소드를 추가

추가하면(자) , 빌드 해 주세요. 에러가 없으면 ,[ClassView] 그리고 CSampleCtrl 를 오른쪽 클릭해 , 표시되는 pop-up menu로부터 [접속 포인트의 인플리맨트] 를 선택합니다.
[connection 포인트의 인플리맨트] 다이얼로그가 표시되므로 , [인터페이스] 에 표시되고 있는 _ISampleCtrlEvents 에 체크를 넣어[OK] 를 클릭합니다.

connection 포인트의 인플리맨트

추가가 끝나면(자) , 재차 빌드 해 주세요. 환경에 의한일지도 모릅니다만 , 여기서
c:\documents and settings\bono\my documents\visual studio projects\atlctrlsample\samplectrl.h(83) :
 error C2065: 'IID__ISampleCtrlEvents' : 정의되어 있지 않은 식별자입니다. 
c:\documents and settings\bono\my documents\visual studio projects\atlctrlsample\samplectrl.h(83) :
 error C2440: 'static_cast' : 'class CSampleCtrl *' (으)로부터 'class ATL::_ICPLocator *' 로 변환할 수 없습니다. 
 (새로운 동작 ; 梏璟을 참조)
        지시받은 형태는 관련이 없습니다; 변환에는 reinterpret_cast, C 습꾸 랸식또는 함수습꾸의 것랸식이 필요합니다. 
c:\documents and settings\bono\my documents\visual studio projects\atlctrlsample\samplectrl.h(83) :
 fatal error C1903: 직전의 것닝걋� 수복할 수 없습니다; 붰諫꾸를 중지합니다. 
(와)과 같은 에러가 나오는 경우 ,CSampleCtrl 클래스의 정의내의 것
BEGIN_CONNECTION_POINT_MAP(CSampleCtrl)
    CONNECTION_POINT_ENTRY(IID_IPropertyNotifySink)
    CONNECTION_POINT_ENTRY(IID__ISampleCtrlEvents)
END_CONNECTION_POINT_MAP()
BEGIN_CONNECTION_POINT_MAP(CSampleCtrl)
    CONNECTION_POINT_ENTRY(IID_IPropertyNotifySink)
    CONNECTION_POINT_ENTRY(DIID__ISampleCtrlEvents)
END_CONNECTION_POINT_MAP()
과 같이 (IID__ISampleCtrlEvents 의 머리에 D 를 부가하는) 편집해 주세요. 빌드 할 수 있게 됩니다.
덧붙여서 , 이것은 위저드의 문제와 같은 생각도 듭니다만 , 나의 찾는 방법이 나쁜 것인지 , 마이크로소프트의 서포트 기술 정보에서의 보고를 찾아낼 수가 없었습니다.
메소드의 추가
접속 포인트의 테스트를 겸해 , 컨트롤에 메소드를 추가해 , 메소드의 코드로부터 접속 포인트를 사용해 이벤트를 발생시킵니다.
우선 , 컨트롤에 메소드를 추가합니다.
[ClassView] 그리고 ISampleCtrl 를 오른쪽 클릭해 , pop-up menu로부터 메소드의 추가를 선택합니다.
[인터페이스에 메소드를 추가] 다이얼로그가 표시되므로 ,[메소드명] 에 MyCallMethod 설정해 , [OK] 를 클릭합니다.

인터페이스에 메소드를 추가

다음에 , 이벤트 통지할 때에 사용하는 ISampleParam 인터페이스 포인터를 생성/파기하는 코드를 추가합니다.
CSampleCtrl 의 클래스 정의에
    ISampleParam* m_pSampleParam;
를 추가합니다.
    // ISampleParam (을)를 보관 유지한다. 
    ISampleParam* m_pSampleParam;
};

#endif //__SAMPLECTRL_H_
CSampleCtrl 의 constructor    을 이하와 같이 편집합니다.
    CSampleCtrl() :
        m_pSampleParam(0)
    {
    }
CComObjectRootEx::FinalConstruct() (을)를 오버라이드(override) 해 , 이하와 같이 추가합니다.
    HRESULT FinalConstruct()
    {
        AFX_MANAGE_STATE(AfxGetStaticModuleState());

        // 이벤트 통지에 사용하는 인터페이스를 작성한다. 
        HRESULT hr;
        hr = CoCreateInstance(CLSID_SampleParam,
                         0,
                         CLSCTX_INPROC_SERVER,
                         IID_ISampleParam,
                         (void **) &m_pSampleParam);
        if (FAILED(hr)) {
            TRACE("FAILURE: FinalConstruct() - CoCreateInstance(): IID_ISampleParam\n");
        }

        return hr;
    }
CComObjectRootEx::FinalRelease() (을)를 오버라이드(override) 해 , 이하와 같이 추가합니다.
    void FinalRelease()
    {
        AFX_MANAGE_STATE(AfxGetStaticModuleState());

        // 이벤트 통지에 사용하는 인터페이스를 해방한다.
        if (m_pSampleParam) {
            m_pSampleParam->Release();
        }
    }
위저드에 의해 CSampleCtrl 에 추가되었다 MyCallMethod 를 이하와 같이 편집합니다.
STDMETHODIMP CSampleCtrl::MyCallMethod()
{
    AFX_MANAGE_STATE(AfxGetStaticModuleState())

    CString msg = _T("―");
    if (m_pSampleParam) {
        m_pSampleParam->put_Msg(msg.AllocSysString());
        Fire_SampleMethod(m_pSampleParam);
    }

    return S_OK;
}
편집이 끝나면(자) , 빌드 해 주세요.
빌드가 끝나면(자) , 또 다시 VB 를 사용해 테스트해 보겠습니다. 폼에 컨트롤 (SampleCtrl1) 과 버튼 (Command1) 을 추가합니다. 버튼을 더블 클릭 해 Command1_Click 이벤트를 , 컨트롤을 더블 클릭 해 SampleCtrl1_SampleMethod 이벤트를 추가해 , 이하와 같이 편집합니다.
Private Sub Command1_Click()
    SampleCtrl1.MyCallMethod
End Sub

Private Sub SampleCtrl1_SampleMethod(ByVal sampParam As ATLCTRLSAMPLELibCtl.ISampleParam)
    MsgBox sampParam.Msg
End Sub
실행해 Command1 를 클릭해 ,"―" (와)과 메시지 박스에 표시되면 OK 입니다.

VB 에서의 실행예

thread의 실장
컨트롤의 메소드 중(안)에서 시간이 걸리는 처리를 실행하고 싶은 것이 있습니다.
메소드내에서 시간이 걸리는 처리를 실시하면 , 컨테이너측 (테스트로 사용했다 VB 나 IE 등 컨트롤의 부모가 되는 어플리케이션) 의 메시지 루프가 돌지 않게 되어 ,UI 가 굳어져 버립니다.
시험삼아 ,CSampleCtrl::MyCallMethod() 의 안에 적당한 Sleep() 을 넣어 , 조금 전의 "―" (을)를 표시시키는 테스트 프로그램을 실행해 보면 압니다. MyCallMethod() (을)를 호출하자 마자 UI 가 응답하지 않게 되는 것을 알 수 있다고 생각합니다.

MyCallMethod() 중(안)에서 메시지 루프를 돌리는 것으로 , 응답하지 않게 되는 것을 막을 수도 있습니다만 , 예를 들면 MyCallMethod() 이 UI 멈춘 구 관계없는 처리를 하는 메소드였던 경우 , 코드안에 UI 를 위한 메시지 루프가 섞여 오는 것은 너무 아름답지 않고 , 기쁜 것은 아니다고 생각합니다.

그렇다고 하는 것으로 , thread화해 보겠습니다. thread화하기에 즈음해 주의하는 것은 ,
  • thread의 시작으로 CoInitialize() 를 불러 , 마지막으로 CoUninitialize() 를 부르는 것.
  • thread에 인터페이스 포인터를 건네줄 때는 마샬링 하는 것.
  • 컨테이너 (컨트롤의 부모가 되는 어플리케이션) 이 종료되었을 때에 thread를 정상적으로 종료할 수 있도록(듯이) 해 두는 것.
의 3 점입니까.
덧붙여 이하의 샘플에서는 시간이 걸리는 처리 대신에 Sleep() 를 사용하고 있습니다.

우선 , thread로서 동작하는 정적 메소드를 클래스에 추가합니다. [ClassView] 그리고 CSampleCtrl 를 오른쪽 클릭해 , pop-up menu로 ,[멤버 함수의 추가] 를 선택합니다. [멤버 함수의 추가] 다이얼로그로 ,[함수의 형태] 에 UINT,[함수의 선언] 에 TestThread(VOID *pParam), [Static] 에 체크를 넣어[OK] 를 클릭해 주세요.
※여기에서는 ,AfxBeginThread() 을 사용해 thread를 작성하기 위해(때문에) , thread 함수의 prototype는
UINT MyControllingFunction(LPVOID pParam);
과 같이 됩니다.

멤버 함수의 추가

다음에 , thread 측에 건네주는 인터페이스를 마샬링 하는데 사용하는 IStream 에의 포인터 변수를 CSampleCtrl 의 멤버 변수로서 추가합니다.
    // ISampleParam 마샬링용
    IStream* m_IStreamSampleParam;
    // 접속 포인트 마샬링용
    CArray<IStream*, IStream*> m_IStreamDispatchArray;
};

#endif //__SAMPLECTRL_H_
그리고 ,CArray 를 사용하기 위해서 StdAfx.h 에
#include <afxtempl.h>
를 추가합니다.
#include <atlcom.h>
#include <atlctl.h>
#include <afxtempl.h>

//{{AFX_INSERT_LOCATION}}
// Microsoft Visual C++ (은)는 전행의 직전에 추가의 선언을 삽입합니다. 

#endif // !defined(AFX_STDAFX_H__55503BB6_11A0_4AE2_ABC9_C341A38E43F7__INCLUDED)
다음에 , 컨테이너 종료시에 thread가 아직 동작하고 있었을 경우 , thread의 종료를 지시하는 플래그와 thread의 종료를 만나고 하기 위해서 , thread 핸들을 퇴피해 두는 변수를 CSampleCtrl 의 멤버 변수로서 추가하는
    // 종료 플래그
    BOOL m_bAbort;
    // thread 종료 만나고 감시용
    HANDLE m_hThread;
};

#endif //__SAMPLECTRL_H_
상기로 추가한 멤버 변수의 초기화를 CSampleCtrl 의 constructor    에 추가합니다.
    CSampleCtrl() :
        m_pSampleParam(0),
        m_IStreamSampleParam(0),
        m_bAbort(FALSE),
        m_hThread(0)
    {
MyCallMethod (을)를 이하와 같이 편집합니다.
STDMETHODIMP CSampleCtrl::MyCallMethod()
{
    AFX_MANAGE_STATE(AfxGetStaticModuleState())

    HRESULT hr;
    IStream* pStream;

    try {

        if (m_hThread) {
            // 이전에 퇴피한 thread 핸들이 남아 있으면 삭제한다. 
            CloseHandle(m_hThread);
            m_hThread = 0;
        }

        //
        // 접속 인터페이스를 마샬링 해 ,m_IStreamDispatchArray 에 격납한다. 
        //
        m_IStreamDispatchArray.RemoveAll();

        CProxy_ISampleCtrlEvents<CSampleCtrl>* pT = static_cast<CProxy_ISampleCtrlEvents<CSampleCtrl>*>(this);
        int nConnectionIndex;
        int nConnections = pT->m_vec.GetSize();

        // 접속 인터페이스를 모두 취득해 , 마샬링 후 , 배열에 격납한다. 
        for (nConnectionIndex = 0; nConnectionIndex < nConnections; nConnectionIndex++) {
            Lock();
            CComPtr<IUnknown> sp = pT->m_vec.GetAt(nConnectionIndex);
            Unlock();
            IDispatch* pDispatch = reinterpret_cast<IDispatch*>(sp.p);
            if (pDispatch != NULL) {
                // 취득했다 IDispatch 를 IStream 에 변환해 격납한다. 
                pStream = 0;
                hr = CoMarshalInterThreadInterfaceInStream(IID_IDispatch,
                            pDispatch, (IStream**) &pStream);
                if (FAILED(hr)) {
                    TRACE("FAILURE: CSampleCtrl::MyCallMethod() - CoMarshalInterThreadInterfaceInStream(): IID_IDispatch\n");
                } else {
                    ASSERT(pStream);
                    m_IStreamDispatchArray.Add(pStream);
                }
            }
        }    

        //
        // ISampleParam 인터페이스를 마샬링 해 , 격납한다. 
        //
        m_IStreamSampleParam = 0;
        
        if (m_pSampleParam) {
            // 마샬링 해 격납한다. 
            pStream = 0;
            hr = CoMarshalInterThreadInterfaceInStream(IID_ISampleParam,
                        m_pSampleParam, (IStream**) &pStream);
            if (FAILED(hr)) {
                TRACE("FAILURE: CSampleCtrl::MyCallMethod() - CoMarshalInterThreadInterfaceInStream(): IID_ISampleParam\n");
            } else {
                ASSERT(pStream);
                m_IStreamSampleParam = pStream;
            }
        }

        //
        // thread를 작성해 , 개시한다. 
        //

        // thread를 작성한다. 
        CWinThread* pThread = AfxBeginThread(
                        TestThread,
                        this,
                        THREAD_PRIORITY_NORMAL,
                        0,
                        CREATE_SUSPENDED,
                        0);
        if (!pThread) {
            return Error(_T("thread를 작성할 수 없었습니다. "));
        }

        // thread 종료 만난 것 , thread 핸들을 복제한다. 
        DuplicateHandle(
            GetCurrentProcess(),    // 원 프로세스
            pThread->m_hThread,        // 오리지날 핸들
            GetCurrentProcess(),    // 처 프로세스
            &m_hThread,                // 복제한 핸들이 카피되는
            0,                        // 액세스권(무시되는)
            FALSE,                    // 계승하지 않는
            DUPLICATE_SAME_ACCESS    // 복제처 핸들은 , 복제원 핸들과 같은 액세스권을 가진다
        );

        m_bAbort = FALSE;

        // thread를 개시한다. 
        pThread->ResumeThread();


    } catch (CException* e) {
        e->Delete();
        return Error(_T("치명적인 에러가 발생했습니다. "));
    }

    return S_OK;
}
오버라이드(override) 한 FinalRelease() 이하와 같이 편집합니다.
    void FinalRelease()
    {
        AFX_MANAGE_STATE(AfxGetStaticModuleState());

        // thread의 종료 상태 만나고 용무의
        // 복제된 thread 핸들이 있으면 ,
        // 만나고를 실시한 뒤 , 삭제한다. 
        try {
            m_bAbort = TRUE;
            if (m_hThread) {
                // NOTE: 타임 아웃을 INFINITE 로 하는 용기가 없기 때문에 ,
                //       적당한 시간을 설정해 있다. 
                WaitForSingleObject(m_hThread, 10000);
                CloseHandle(m_hThread);
            }
        } catch (CException* e) {
            e->Delete();
        }

        // 이벤트 통지에 사용하는 인터페이스를 해방한다. 
        if (m_pSampleParam) {
            m_pSampleParam->Release();
        }
    }
마지막에 TestThread() 를 이하와 같이 편집합니다.
UINT CSampleCtrl::TestThread(VOID *pParam)
{
    AFX_MANAGE_STATE(AfxGetStaticModuleState());

    // COM 라이브러리를 초기화해 , 새로운 아파트먼트스렛드에 들어간다. 
    if (FAILED(CoInitialize(0))) {
        return 1;
    }

    // 클래스 인스턴스에의 포인터를 꺼낸다. 
    CSampleCtrl* pSampleCtrl = (CSampleCtrl*) pParam;
    if (!pSampleCtrl) {
        return 1;
    }

    // 시간이 걸리는 처리를 한다. 
    for (int i = 0; i < 10; ++i) {
        if (pSampleCtrl->m_bAbort) {
            break;
        }
        Sleep(1000);
    }

    if (!pSampleCtrl->m_bAbort) {
        // 컨테이너 종료시 이외는 통지한다. 
        HRESULT hr;
        IStream* pStream;
        int i;

        //
        // 컨테이너에 이벤트 통지한다. 
        //
        // 안 마샬링 후의 접속 포인트를 격납한다. 
        CArray<IDispatch*, IDispatch*> dispatchArray;

        // 안 마샬링 후의 ISampleParam 인터페이스를 격납한다. 
        ISampleParam* pSampleParam = 0;

        //
        // 접속 인터페이스를 안 마샬링 해 배열에 격납한다. 
        //
        for (i = 0; i < pSampleCtrl->m_IStreamDispatchArray.GetSize(); ++i) {
            pStream = pSampleCtrl->m_IStreamDispatchArray[i];
            IDispatch* pDispatch = 0;
            hr = CoGetInterfaceAndReleaseStream(pStream,
                    IID_IDispatch, (void**) &pDispatch);
            if (FAILED(hr)) {
                TRACE("FAILURE: CSampleCtrl::TestThread() - CoGetInterfaceAndReleaseStream(): IID_IDispatch\n");
            } else {
                ASSERT(pDispatch);
                dispatchArray.Add(pDispatch);
            }
        }

        //
        // ISampleParam 인터페이스를 안 마샬링 해 , 격납한다. 
        //
        pSampleParam = 0;

        pStream = pSampleCtrl->m_IStreamSampleParam;
        hr = CoGetInterfaceAndReleaseStream(pStream,
                    IID_ISampleParam, (void**) &pSampleParam);
        if (FAILED(hr)) {
            TRACE("FAILURE: CSampleCtrl::TestThread() - CoGetInterfaceAndReleaseStream(): IID_IResultEndPrint\n");
        } else {
            ASSERT(pSampleParam);
        }


        // ISampleParam 인터페이스에 값을 설정해 이벤트를 발생한다. 
        if (pSampleParam) {
            CComVariant varResult;
            CString msg = _T("헬로");
            pSampleParam->put_Msg(msg.AllocSysString());

            CComVariant* pvars = new CComVariant[1];
            for (i = 0; i < dispatchArray.GetSize(); ++i) {
                VariantClear(&varResult);
                IDispatch* pDispatch = dispatchArray[i];
                pvars[0] = pSampleParam;
                DISPPARAMS disp = { pvars, NULL, 1, 0 };
                pDispatch->Invoke(0x1, IID_NULL, LOCALE_USER_DEFAULT, DISPATCH_METHOD, &disp, &varResult, NULL, NULL);
            }
            delete[] pvars;
            // varResult.scode 의 값은 버린다. 
        }
    }

    // COM 라이브러리를 해방한다. 
    CoUninitialize();

    return 0;
}
컨테이너 종료시에 통지하지 않게 하고 있는 것은 , 타이밍에 따라서는 , 친측의 인스턴스가 소멸하고 있을 가능성이 있어 , 통지를 실시하는 것에 의해 액세스 위반이 일어나기 (위해)때문입니다.
편집이 끝나면(자) , 빌드 해 조금 전과 같이 VB 등에서 테스트해 보세요.
UI 의 응답이 굳어질 것도 없고 , 한편 , thread 처리가 종료하면(자) , 이벤트가 발생하는 것이 확인할 수 있다고 생각합니다.

덧붙여 이 샘플에서는 thread내의 플래그의 배타적 연산이나 thread의 이중 기동등에는 모두 대처하고 있지 않습니다. 실제로 업무로 사용하는 경우는 , 그것들을 고려할 필요가 있다고 생각되기 때문에 , 조심해 주세요.

※위의 샘플을 HTML 에 묻어 , thread로부터 발생시킨 이벤트 통지 중(안)에서 javascript 등에서 window.close() 를 실시하면 , 가끔 , 이상종료(ABEND) 해 버리는 것을 확인하고 있습니다.
원인은 잘 모릅니다만 , 액세스 위반이 원인 같므로 , thread 종료전에 , thread를 기동한 측의 인스턴스가 무효가 되어 있는 것이라고 생각합니다.
javascript 의 setTimeout() 를 사용해 ,setTimeout("exitfunc", 2000) 와 같이 해 , 이벤트 핸들러의 제어를 컨트롤에 되돌리고 나서 ,exitfunc() 안에서 window.close() 하도록 하면 , thread를 어떻게든 정상 종료시킬 수가 있는 것 같아 , 액세스 위반은 일어나지 않게 됩니다. 다만 이것은 , thread의 종료 타이밍에 의존한 회피책이므로 완전한 것은 않고 , 본래는 컨트롤측에서 대처해야 하는 것입니다만 , 근본적인 해결책을 찾아낼 수 있고 있지 않습니다. 만약 ,HTML 에 묻어 이용이 되는 경우는 충분히 주의해 주십시오.
샘플 소스
atlctrlsample.zip (36,072 아르바이트)

잡동사니 시장에