2015. 7. 29. 17:12

ActiveX(엑티브엑스) 개발 가이드

말도 많고 탈도 많은 ActiveX 얘기 인데요...

Windows 10 Edge(엣지)도 그러하지만, 점점 ActiveX는 줄어들 거라 보여집니다.

허나, 국내 업체에서는 여전히 AcitveX를 통한 기능 요구들을 합니다. 잘 사용하지 않는 ActiveX이지만, 갑작스러운 고객의 요청에 빨리 ActiveX를 개발하는 가이드를 공유합니다. 일단, AcitveX 특징상, 배포의 이슈가 있는데, 재배포팩등 여러 이슈등으로 Visual Studio 6.0 기준으로 작업을 설명합니다. 물론, 비슷한 방법으로 그 이상 버전의 Visual Studio에서도 작업이 이뤄질 겁니다.


Project 생성


아래와 같이 Project를 생성합니다.



Has an "About" box는 Off해도 됩니다. 이외 Advanced에 나타나는 Option도 체크해 보시기 바랍니다.

(큰 변화는 없는듯 합니다.)


Finish를 통해 Project를 생성합니다.


Method 추가


위와 같이 Add Method하여 AcitveX에 Method를 추가합니다.

위와 같이 함수명과 리턴값을 지정합니다. Parameter도 추가 가능합니다.


그리고, Test_AXCtl.cpp에 아래와 같이 Code를 넣습니다.

/////////////////////////////////////////////////////////////////////////////
// CTest_AXCtrl message handlers

BOOL CTest_AXCtrl::CallActiveX()
{
    ::MessageBoxA(NULL, "I'm Win32", "I'm Win32", MB_OK);

    return TRUE;
}


그리고 빌드합니다. 그러면, Test_AX.ocx 파일이 생성됩니다.

만일, 외부 배포를 하려는 용도라면, Win32 Release 혹은 Win32 Unicode Release를 이용하시기 바랍니다. Debug관련 Dependency가 누락될 수 있습니다.


Test html page 확인하기


Test_AXCtl.cpp에 아래와 같은 코드가 있습니다.

////////////////////////////////////////////////////////////////////////////
// Initialize class factory and guid

IMPLEMENT_OLECREATE_EX(CTest_AXCtrl, "TESTAX.TestAXCtrl.1",
    0x46bcfd6a, 0x3b25, 0x436d, 0x8f, 0x4c, 0x3, 0x97, 0xa8, 0xc2, 0xaf, 0x45)

위에 있는 GUID 코드를 다음과 같이 표현하여, Notepad등에 적어둡니다.

46bcfd6a-3b25-436d-8f4c-0397a8c2af45


cmd.exe를 관리자 권한으로 실행한다음, Test_AX.ocx 경로로 cd한다음, regsvr32.exe Test_AX.ocx를 실행하여 ActiveX를 등록합니다. 그럼 "Test_AX.ocx에서 DllRegisterServer이(가) 성공했습니다." 창이 뜰 겁니다.


이제, Test_AX.ocx 경로에 다음과 같은 Test.html 파일을 생성합니다.


<HTML>
<HEAD>
<SCRIPT LANGUAGE="JavaScript" FOR="window" EVENT="onLoad()">
<!--
if (!TEST_AX.CallActiveX())
{
    alert('CallActiveX return 0');
}
else
{
    alert('CallActiveX return 1');
}
-->
</SCRIPT>
<TITLE>New Page</TITLE>
</HEAD>
<BODY>
    <div style="display:none;">
        <OBJECT ID="TEST_AX" WIDTH=0 HEIGHT=0 CLASSID="CLSID:46bcfd6a-3b25-436d-8f4c-0397a8c2af45"></OBJECT>
    </div>
</BODY>
</HTML>


해당 html을 IE에서 실행하면,

(이 페이지의 ActiveX 컨트롤이 다른 부분과 상호 작용하는데 안전하지 않을 수 있습니다. 상호 작용을 허용하시겠습니까?)

가 뜨고, "예"를 누릅니다.

(만일 이 경고창을 없애고 싶은 경우(아마도 대다수일듯), 맨 아랫 부분을 참고)


그럼,

가 뜨고, 확인을 누르면,

가 뜹니다.

이는 html의 JavaScript의 Alert로 띄운 것으로, ActiveX의 함수 호출의 리턴값을 Web(즉, JavaScript)에서 받아 표시하고 있다는 건데, 즉, ActiveX와 Web간의 정보 교류가 가능하다는 뜻입니다.


이걸 설명하자면, IE 브라우져의 html에서, Win32 API로 명령된 "I'm Win32" 창을 띄운 겁니다. 이는 ::MessageBoxA라는 Win32 API를 Web에서 호출한 것인데, 즉, HTML web에서 ActiveX를 이용하여 Win32 API를 호출한 것입니다. 다시말해, html에서 시스템의 파일이나 레지스트리등을 수정할 수 있다는 것으로, 이건 당연히 웹 표준이 절대 아닙니다. 웹은 브라우져라는 sanbox에서 실행되어, 시스템에 영향을 주어서는 안되겠죠. 그렇지만, 국내 IT 여건상, 고객들은 AcitveX를 제작해달라는 요청을 하기도 합니다. 웹 표준을 벗어나는 요구를 해서 그렇습니다. 어쩔수 없죠.


cab으로 묶기


위과 같은 방법으로 대략 Test를 했다면, 이제 ActiveX를 cab으로 묶어 배포를 해야 합니다. 이를 위해서는 "전자 서명"이 필요한데, 일단, 현재는 전자 서명 과정은 Skip하겠습니다.


cab으로 묶기 위해서는 cabarc.exe가 필요한데, 없다면 아래를 참고하세요.


CabArc.zip

그럼 아래와 같이 임의의 경로에 파일을 구성하세요.

Test_AX.ocx는 위에서 빌드된 파일이며, Test_AX.readme.txt는 ActiveX .ocx 파일과 함께 배포할 파일입니다. 보통 .ocx이외 .exe나 .dll 혹은 .jpg같은 파일도 함께 .cab으로 묶에 배포 가능함을 알려드립니다. Test_AX.inf는 .cab으로 묶기 위한 정보와 AcitveX 설치 정보를 포함하는 파일입니다.


Test_AX.inf는 다음과 같습니다.

[version]
; version signature (same for both NT and Win95) do not remove
signature="$CHICAGO$"
AdvancedINF=2.0

[DefaultInstall]
; Tells which file to install
CopyFiles=FileList
RegisterOCXs=ActiveXList

[DefaultUninstall]
UnRegisterOCXs=ActiveXList
DelFiles=DelFileList

[DestinationDirs]  
; Destination Directories for CopyFiles/Delfiles Sections.
; 10 indicates - windows directory  
; 24 indicates - Root directory of the drive containing the Windows directory  
FileList = 24,%PROGRAMF%\greenfish\AX
DelFileList = 24,%PROGRAMF%\greenfish\AX\
 
[FileList]
; ,,,32 - Suppress version conflict dialog and don't overwrite newer DLLs
Test_AX.ocx,,,32
Test_AX.readme.txt,,,32

[DelFileList]
; ,,,1 - If the file is in use, queue up a delayed delete operation in Wininit.ini.
;        Otherwise, a file that is currently in use won't be deleted.
Test_AX.ocx,,,1
Test_AX.readme.txt,,,1

[ActiveXList]
%24%\%PROGRAMF%\greenfish\AX\Test_AX.ocx

[Strings]
PROGRAMF = "PROGRA~1"


Test_AX.ocx와 Test_AX.readme.txt를 C:\Program Files\greenfish\AX에 복사하는 내용입니다.

자세한건 google에 검색해 보세요.


이제, _cab.bat는 다음과 같이 작성합니다.

@echo off

@rem cab으로 묶는다.
%CD%\CABARC.exe -s 6144 n "%CD%\TEST_AX.cab" "%CD%\TEST_AX.ocx" "%CD%\Test_AX.readme.txt" "%CD%\TEST_AX.inf"

pause


그다음, _cab.bat를 실행하면, TEST_AX.cab 파일이 마침내 성성됩니다.



Microsoft (R) Cabinet Tool - Version 5.00.2134.1
Copyright (C) Microsoft Corp. 1981-1999.

Creating new cabinet 'E:\Temp\_activex_\Cab\TEST_AX.cab' with compression 'MSZIP':
  -- adding E:\Temp\_activex_\Cab\TEST_AX.ocx
  -- adding E:\Temp\_activex_\Cab\Test_AX.readme.txt
  -- adding E:\Temp\_activex_\Cab\TEST_AX.inf

Completed successfully
계속하려면 아무 키나 누르십시오 . . .


그리고, 아까전 관리자 권한으로 실행했던, cmd창에 regsvr32.exe -u Test_AX.ocx를 실행하여 ActiveX 등록을 해제합니다.


이제 Test.html 파일이 있던 경로에, TEST_AX.cab 파일을 복사합니다. 그리고, Test.html 파일을 다음의 붉은색 부분을 추가합니다.


<HTML>
<HEAD>
<SCRIPT LANGUAGE="JavaScript" FOR="window" EVENT="onLoad()">
<!--
if (!TEST_AX.CallActiveX())
{
    alert('CallActiveX return 0');
}
else
{
    alert('CallActiveX return 1');
}
-->
</SCRIPT>
<TITLE>New Page</TITLE>
</HEAD>
<BODY>
    <div style="display:none;">
        <OBJECT ID="TEST_AX" WIDTH=0 HEIGHT=0 CLASSID="CLSID:46bcfd6a-3b25-436d-8f4c-0397a8c2af45" CODEBASE="TEST_AX.cab"></OBJECT>
    </div>
</BODY>
</HTML>

(.html 파일과 .cab 파일의 경로가 다르다면, CodeBase에 그 상대 경로를 전달합니다.)


그리고, 이제 .html을 실행하면,

과 같이 UAC가 발생하며, 설치합니다.

(만일, .cab으로 묶기전 Test_AX.ocx를 전자서명하고, cab으로 묶은뒤, 해당 .cab 파일을 전자서명하면 위와 다르게 녹색의 좀더 부드러운 문구의, UAC 창으로 알림됩니다.)

"예"를 누르고, I'm Win32 메시지를 받을 수 있습니다.


그리고, IE의 추가기능관리에 들어가면 아래와 같이 등록됨이 확인됩니다.

위와 같이 C:\Program Files\greenfish\AX 경로에 Test_AX.ocx와 Test_AX.readme.txt 파일이 들어감이 확인됩니다.

이제, 삭제를 테스트하자면, 모든 IE를 닫고, 새 IE를 실행합니다. 그리고, 추가기능관리에 들어가,

로 하면, Test_AX Control이 표시되며, 더블클릭하여 아래와 같이 삭제할 수 있습니다.

그럼, 위 Test_AX.ocx와 Test_AX.readme.txt는 삭제됩니다. (만일 해당 ActiveX가 Load되어 있다면, Test_AX.ocx는 삭제되지 않고 남아있게 됩니다.)


이상으로 ActiveX 개발 가이드를 마치며, 포스트는 했지만, 앞으로 더이상 AcitveX 개발은 없기를 기대합니다.


참고) 이 페이지의 ActiveX 컨트롤이 다른 부분과 상호 작용하는데 안전하지 않을 수 있습니다. 제거하기.

Safe Initialization and Scripting for ActiveX Controls를 참고해야 합니다.

쉬운 방법으로, 아래와 같이 정리할 수 있습니다.


우선, DllRegisterServer()등의 함수가 저장되어 있는 Test_AX.cpp 파일만 수정하면 됩니다.


대략, DllRegisterServer(void) 함수 정의전에,

아래 코드를 추가합니다.


//////////////////////////////////////////////////////////////////////////
// Safe Initialization and Scripting for ActiveX Controls
// https://msdn.microsoft.com/en-us/library/aa751977%28v=vs.85%29.aspx
#include <comcat.h>
#include <ObjSafe.h>
#include <strsafe.h>
HRESULT CreateComponentCategory(CATID catid, WCHAR *catDescription)
{
    ICatRegister *pcr = NULL ;
    HRESULT hr = S_OK ;
    
    hr = CoCreateInstance(CLSID_StdComponentCategoriesMgr,
        NULL, CLSCTX_INPROC_SERVER, IID_ICatRegister, (void**)&pcr);
    if (FAILED(hr))
        return hr;
    
    // Make sure the HKCR\Component Categories\{..catid...}
    // key is registered.
    CATEGORYINFO catinfo;
    catinfo.catid = catid;
    catinfo.lcid = 0x0409 ; // english
    unsigned int len;
    // Make sure the provided description is not too long.
    // Only copy the first 127 characters if it is.
    // The second parameter of StringCchLength is the maximum
    // number of characters that may be read into catDescription.
    // There must be room for a NULL-terminator. The third parameter
    // contains the number of characters excluding the NULL-terminator.
    hr = StringCchLength(catDescription, STRSAFE_MAX_CCH, &len);
    if (SUCCEEDED(hr))
       {
        if (len>127)
        {
            len = 127;
        }
    }   
    else
    {
        // TODO: Write an error handler;
    }
    // The second parameter of StringCchCopy is 128 because you need
    // room for a NULL-terminator.    
    hr = StringCchCopy(catinfo.szDescription, len + 1,
        catDescription);
    // Make sure the description is null terminated.
    catinfo.szDescription[len + 1] = '\0';
    
    hr = pcr->RegisterCategories(1, &catinfo);
    pcr->Release();
    
    return hr;
}

HRESULT RegisterCLSIDInCategory(REFCLSID clsid, CATID catid)
{
    // Register your component categories information.
    ICatRegister *pcr = NULL ;
    HRESULT hr = S_OK ;
    hr = CoCreateInstance(CLSID_StdComponentCategoriesMgr,
        NULL, CLSCTX_INPROC_SERVER, IID_ICatRegister, (void**)&pcr);
    if (SUCCEEDED(hr))
    {
        // Register this category as being "implemented" by the class.
        CATID rgcatid[1] ;
        rgcatid[0] = catid;
        hr = pcr->RegisterClassImplCategories(clsid, 1, rgcatid);
    }
    
    if (pcr != NULL)
        pcr->Release();
    
    return hr;
}

HRESULT UnRegisterCLSIDInCategory(REFCLSID clsid, CATID catid)
{
    ICatRegister *pcr = NULL ;
    HRESULT hr = S_OK ;
    
    hr = CoCreateInstance(CLSID_StdComponentCategoriesMgr,
        NULL, CLSCTX_INPROC_SERVER, IID_ICatRegister, (void**)&pcr);
    if (SUCCEEDED(hr))
    {
        // Unregister this category as being "implemented" by the class.
        CATID rgcatid[1] ;
        rgcatid[0] = catid;
        hr = pcr->UnRegisterClassImplCategories(clsid, 1, rgcatid);
    }
    
    if (pcr != NULL)
        pcr->Release();
    
    return hr;
}


그리고, 그 다음에, 아래 코드를 작성합니다.

이는 앞서 등록했던 clsid 값임을 명심해야 합니다.


const CATID CLSID_SafeItem =
{ 0x46bcfd6a, 0x3b25, 0x436d,{ 0x8f, 0x4c, 0x3, 0x97, 0xa8, 0xc2, 0xaf, 0x45}};
 


그리고 아래와 같이, DllRegisterServer(void)와 DllUnregisterServer(void) 함수를 수정합니다.


STDAPI DllRegisterServer(void)
{
    HRESULT  hr;    // return for safety functions

    AFX_MANAGE_STATE(_afxModuleAddrThis);

    if (!AfxOleRegisterTypeLib(AfxGetInstanceHandle(), _tlid))
        return ResultFromScode(SELFREG_E_TYPELIB);

    if (!COleObjectFactoryEx::UpdateRegistryAll(TRUE))
        return ResultFromScode(SELFREG_E_CLASS);
   
    // Mark the control as safe for initializing.
   
    hr = CreateComponentCategory(CATID_SafeForInitializing, L"Controls safely initializable from persistent data!");
        if (FAILED(hr))
            return hr;
       
    hr = RegisterCLSIDInCategory(CLSID_SafeItem, CATID_SafeForInitializing);
    if (FAILED(hr))
        return hr;
       
    // Mark the control as safe for scripting.
   
    hr = CreateComponentCategory(CATID_SafeForScripting, L"Controls safely scriptable!");
        if (FAILED(hr))
            return hr;
       
    hr = RegisterCLSIDInCategory(CLSID_SafeItem, CATID_SafeForScripting);
    if (FAILED(hr))
        return hr;
           
    return NOERROR;
}
 


STDAPI DllUnregisterServer(void)
{
    HRESULT hr;    // HResult used by Safety Functions

    AFX_MANAGE_STATE(_afxModuleAddrThis);

    if (!AfxOleUnregisterTypeLib(_tlid))
        return ResultFromScode(SELFREG_E_TYPELIB);

    if (!COleObjectFactoryEx::UpdateRegistryAll(FALSE))
        return ResultFromScode(SELFREG_E_CLASS);
   
    // Remove entries from the registry.
   
    hr=UnRegisterCLSIDInCategory(CLSID_SafeItem, CATID_SafeForInitializing);
    if (FAILED(hr))
        return hr;
   
    hr=UnRegisterCLSIDInCategory(CLSID_SafeItem, CATID_SafeForScripting);
    if (FAILED(hr))
        return hr;
   
    return NOERROR;
}
 


이 수정으로 빌드된 ActiveX는 위와 같은 경고창이 발생하지 않습니다.