배움터  
  HOME > 배움터 > Daily Tip
Daily Tip

제품:   Excel 버전:   공통
검색어:   유저폼 목, 메뉴
제목:   유저폼 목에 메뉴달기
     
 

  STEP> 살펴보기


엑셀 프로그래밍과 관련하여 스프레드시트로서의 본연의 임무를 벗어난 질문이나 요청을 하는 경우가 종종 있습니다. VBA 가지고 윈도우 폼 프로그램처럼 하나의 애플리케이션으로서 만들어 보려는 욕심때문이겠죠.

그러한 욕심 중 하나가 위의 그림과 같이 Userform에 메뉴를 달아보고 싶어하는 것입니다.

그러나 Userform에 메뉴를 다는 것은 고양이 목에 방울 달겠다는 생각처럼 간단하지는 않습니다. 그렇다고 불가능하진 않습니다. 우린 생쥐가 아니라 사람이니까 고양이 붙들고 방울 달아주면 됩니다.

Userform에 메뉴를 달려면 VBA기능만으로는 어렵고 API함수를 사용해야 합니다. 그리고 메뉴를 만드는 API함수와 메뉴를 클릭했을 때 이를 처리해주는 함수를 만들어야 합니다.

다음은 API를 이용하여 메뉴를 달아보는 예입니다. 주의해야 할 점은 종종 디버깅하거나 소스를 만드는 과정에서 윈도우를 다시 부팅해야 하는 경우도 생깁니다. API를 사용하는 것이 시스템에 밀착된 것이라 잘못 사용하면 독(毒)이 되는 것입니다.

구체적으로 대강의 조립순서(레고블럭처럼 생각하시면 편리합니다)을 말하자면, 먼저 FindWindow()함수를 사용하여 Userform의 핸들을 구합니다. 그리고 CreateMenu(), CreatePopupMenu(), AppendMenu()함수를 사용하여 메뉴를 만듭니다. 만들어진 메뉴는 SetMenu()함수를 이용하여 Userform에 붙이게 됩니다.

여기까지 하게 되면 Userform에 메뉴가 보이지만 아직 완성된 것은 아닙니다. 메뉴항목을 클릭하면 원하는 프로시져가 실행되도록 해야 합니다.

그래서 GetWindowLong(), SetWindowLong(), CallWindowProc()함수를 사용하여 메뉴를 클릭하였을 때 원래의 윈도우프로시져를 사용자 정의 함수인 MenuProc()로 바꿔칩니다(이를 서브클래싱이라고 합니다)

Module1.bas

Option Explicit

Public Declare Function CreateMenu Lib "user32" () As Long
Public Declare Function CreatePopupMenu Lib "user32" () As Long
Public Declare Function FindWindow Lib "user32" Alias "FindWindowA" (ByVal lpClassName As String, ByVal lpWindowName As String) As Long
Public Declare Function AppendMenu Lib "user32" Alias "AppendMenuA" (ByVal hMenu As Long, ByVal wFlags As Long, ByVal wIDNewItem As Long, ByVal lpNewItem As Any) As Long
Public Declare Function SetMenu Lib "user32" (ByVal hWnd As Long, ByVal hMenu As Long) As Long 

Public Declare Function GetWindowLong Lib "user32" Alias "GetWindowLongA" (ByVal hWnd As Long, ByVal nIndex As Long) As Long
Public Declare Function SetWindowLong Lib "user32" Alias "SetWindowLongA" (ByVal hWnd As Long, ByVal nIndex As Long, ByVal dwNewLong As Long) As Long
Public Declare Function CallWindowProc Lib "user32" Alias "CallWindowProcA" (ByVal lpPrevWndFunc As Long, ByVal hWnd As Long, ByVal Msg As Long, ByVal wParam As Long, ByVal lParam As Long) As Long 

Public Const MF_STRING = &H0&
Public Const MF_POPUP = &H10&
Public Const GWL_WNDPROC = (-4)
Public Const WM_COMMAND = &H111
Public Const WM_CLOSE = &H10
 

// old window procedure의 주소
Public OldProc          As Long 

// 메뉴항목의 ID
Public Const SUBMENU1   As Long = 1
Public Const SUBMENU2   As Long = 2
Public Const SUBMENU3   As Long = 3
Public Const SUBMENU4   As Long = 4
 

Public Function MenuProc(ByVal hWnd As Long, ByVal wMsg As Long, ByVal wParam As Long, ByVal lParam As Long) As Long 

   Select Case wMsg&
     Case WM_CLOSE
        
// 사용자가 윈도우(여기에선 Userform을 말함)를 닫는 경우
        
// subclassing을 중지하고 원래의 window procedure를
         // 돌려줍니다.
               Call SetWindowLong(hWnd&, GWL_WNDPROC, OldProc&)
      
     Case WM_COMMAND
        // WM_COMMAND가 윈도우(여기에선 Userform을 말함)로
        //
냅니다.
          // 사용자가 메뉴항목을 클릭할때마다 메뉴항목ID가 wParam
          //  에 저장됩니다.              
          
     Select Case wParam&
       
        Case SUBMENU1
                  Call MsgBox("You clicked Menu 1!" , vbExclamation)
         
      Case SUBMENU2
                 
 Call MsgBox("You clicked Menu 2!" , vbExclamation)
                          Unload UserForm1
           
    Case SUBMENU3
                  Call MsgBox("You clicked Menu 3!", vbExclamation)
            
   Case SUBMENU4
                  Call MsgBox("You clicked Menu 4!", vbExclamation)
         
   End Select      
      End Select      

  // 모든 작업을 마치면 기본적인 처리(원도우의 정상적인 작업)을 위해
  // 원래의 window procedure를 다시 호출
합니다.

  MenuProc = CallWindowProc(OldProc&, hWnd&, wMsg&, wParam&,
  lParam&)
End Function
 

다음은 Userform의 소스입니다. Userform에는 아무런 컨트롤이 없습니다.

Userform1.cls

Option Explicit

// userform의 핸들값
Dim hWnd            As Long 

Private Sub UserForm_Initialize()
  Dim hTop    As Long
  Dim hSub    As Long   

  If Val(Application.Version) < 9 Then
   hWnd = FindWindow("ThunderXFrame", Me.Caption)  '// XL97이하
  Else
   hWnd = FindWindow("ThunderDFrame", Me.Caption)  '// XL2000이상   
 
 
End If 

   // 메인메뉴를 만들고 핸들값을 얻습니다.
   hTop = CreateMenu() 

   // 첫번째 부메뉴를 만들고 핸들값을 얻습니다.
   hSub = CreatePopupMenu()   

   // 위에서 얻은 핸들값으로 Parent를 가리킵니다.
   // 따라서 Parent밑에 메뉴항목을 만
듭니다.
   Call AppendMenu(hSub, MF_STRING, SUBMENU1, "&New")
   Call AppendMenu(hSub, MF_STRING, SUBMENU2, "&Exit")   

   // 메인메뉴에 붙입니다.
   // 마찬가지로 위의 hSub는 hTop을 부모로 하여 그 밑에 붙
입니다.
   AppendMenu hTop, MF_POPUP, hSub, "&File" 

   // 두번째 부메뉴
   hSub = CreatePopupMenu()
   Call AppendMenu(hSub, MF_STRING, SUBMENU3, "&Undo")
   Call AppendMenu(hSub, MF_STRING, SUBMENU4, "&Copy")
   AppendMenu hTop, MF_POPUP, hSub, "&Edit"
   // 메인메뉴에 붙입니다. 

   // 메뉴를 윈도우에 표시
   // 메뉴자체를 Userform의 핸들(메뉴의 부모인 셈이다)에 붙
입니다.
   SetMenu hWnd, hTop   

   // old window procedure를 주소를 구합니다.
   OldProc& = GetWindowLong(hWnd, GWL_WNDPROC)   

   // old window procedure주소를 MenuProc주소로 대신
   // MenuProc을 콜백함수하고
합니다.
   // SetWindowLong()이나 EnumWindows()와 같은 함수를 호출할 때
  
// 함수의 인수로서 프로그래머가 만든 함수 포인터를 전달합니다.
   // 이때 인수로 전달되는 함수를 콜백(Callback)함수라고
합니다.
   // 콜백함수는 폼이나 클래스가 아닌 일반모듈에 기술해야
합니다.
   // AddressOf는 함수의 주소(포인터)를 구하는 역할을
합니다.
  Call SetWindowLong(hWnd, GWL_WNDPROC, AddressOf MenuProc)
End Sub
 

 

 
  참고>서브클래싱(Subclassing)
윈도우에서 수행되던 프로시저를 사용자 정의 프로시저로 연결하여 원하는 작업 수행하고 완료되면 다시 원래의 프로시저에게 되돌려주는 기능을 하는 것을 말합니다.