PROGRAMMING WORKSHOP

COMAddIns | VSTO - 엑셀의 메뉴에서 OnAction속성...

질문이 하나 들어와서 처리하도록 하자
이 질문은 당연히 발생하는 것중의 하나다
VBA에서 메뉴를 다루고 메뉴개체에 OnAction 속성에 크릭하면
실행될 프로시져의 이름을 주면 멋지게 실행이 되었었는데
VSTO에서는 먹통이다..
메뉴를 어떻게 만들기는 했는데..크릭하니까..아무짓도 안하거나
에러가 난다..
OnAction속성의 프로시져명은 VB.Net의 모듈에 접근할수 없다
엑셀의 VBA모듈의 것에만 접근하게 만들어진 것이다
그럼 어떻게 하나???
못써먹나??
다른 방법, 더 좋은 방법을 찾으면 된다

우선 CommandBar개체는 소속이 엑셀 소속의 개체가 아니고
Office소속의 개체라는 점 잊지 말자
이것을 무시하면 처음부터 애먹는다..
엑셀에서 아무리 찾아도 없으니까!!!
VSTO템프릿을 사용하면 아래와 같이 필요한 것이 이미 Import되어 있다
그리고 Office는 Office로 접근하면 되고 Excel은 Excel로 접근하면된다



WithEvents 라는 키워드를 VBA에서도 사용하였던 기억이 날 것이다
Application개체는 이벤트가 있는데 아무곳에서도 찾을 수 없었다
그래서 크래스모듈을 넣은후 크래스모듈의 선언부에
WithEvents App As Appliction 이라고 하면 Application개체가 갖고 있는
모든 이벤트목록을 해당 크래스모듈에서 볼수 있었다
이것을 그대로 사용하면 된다

크릭하는 대상이 되는 메뉴는 CommandBarButton 개체이다
그래서 아래와 같이 선언한다

WithEvents oMenuButton As Office.CommandBarButton

이것은 WithEvents oMenuButton As Microsoft.Office.Core.CommandBarButton
인데 위의 그림에서 Office라는 이름으로 Import된 상태이니
그냥 Office.CommandBarButton으로 접근하면 된다
이렇게 하면 크래스모듈에 아래의 그림과 같이 이벤트목록을 볼수 있다



그런데 OnAction 속성같은 것은 필요없는 것이다
메뉴를 크릭하면 그냥 Click 이벤트하나 밖에 없다
이 Click 이벤트프로시져에 하고 싶은 일을 작성하면 된다

Private Sub oMenuButton_Click(ByVal Ctrl As Microsoft.Office.Core.CommandBarButton, ByRef CancelDefault As Boolean) Handles oMenuButton.Click
이곳에 메뉴를 크릭하였을때 하고 싶은 일을 작성하면된다
    MsgBox("메뉴를 크릭하셨습니다")
End Sub

이제 셀을 오른쪽마우스로 크릭할때 나타나는 메뉴를 찾아서
이 메뉴에 자식메뉴를 하나 달아야 한다
이 자식 메뉴가 위에 WithEvents로 선언한 개체이다
VBA에서는 그냥
CommandBars("Cell") 이라고 찾으면 CommandBar개체를 얻는다
개체의 타입과 실제의 개체를 헷갈리지 마셔야 한다
개체의 타입은 Office가 갖고 있는 크래스인 것이다
그리고 실체의 개체는 엑셀이 갖고 있는 개체이다

무슨 소리냐 하면
아래는 어떤 타입의 변수를 선언한 것이다
이때 사용하는 타입이 Office가 갖고 있는 자원을 사용하는 것이고

WithEvents oMenuButton As Office.CommandBarButton
Dim oBar As Office.CommandBar

이 변수에 값을 넣는 것은 이미 엑셀이 갖고 있는 개체를 찾아주어야 한다
아래와 같이

oBar = Globals.ThisAddIn.Application.CommandBars("Cell")
oBar.Reset()
oMenuButton = oBar.Controls.Add(Office.MsoControlType.msoControlButton)
oMenuButton.Caption = "Click Me!!!"

아래와 같이 메뉴가 만들어지고 크릭하면 실행된다



아래의 쏘스원본을 보시고 확장해 보시기 바란다

***[LOG-IN]***

이왕 내친 김에 하나 더 하고 넘어가자..
UserForm의 CommandButton을 워크시트에 심고 크릭할수 있게 할 수 있을까??
이것을 하면
여러분은 많은 VB.Net의 많은 컨셉을 소화시킬수 있는 기회가 될 수 있을 것같다
별로 프로그래밍적으로는 실용적인 방법은 아니지만 학습차원에서는
아주 좋은 과제가 될 것 같다
아래의 그림과 같이 CommandButton을 심는다
엑셀자체의 양식콘트롤은 OnAction의 작업을 할수 없다, 이것은 불가능하다
하지만 UserForm의 ActiveX콘트롤은 처리할수 있는 것이다



VB.Net에서 해보기전에
엑셀에 VBA로 해보도록 하자
하나는 워크시트에 해보고, 다른 하나는 UserForm에 한다
같은 Class모듈을 사용하였는데 안된다
결론적으로 말하면 워크시트에 CommandButton을 만들어지기는 하는데
크릭하면 멍청히 가만히 있는다
하지만 UserForm의 것은 잘 된다



워크시트에서도 논리적으로 되어야 하는데 안된다..그것참!!
아무튼 VBA에서 크래스모듈을 사용하는 것과 VB.Net의 것을
비교하여 보는 것이 좋은 것이니, 안되는 것에 너무 집착하지 않는 것도 좋다
얼릉 다른 길을 찾는 것이 좋다

VBA에서 아래와 같이 크래스모듈을 하나 삽입하고 크라스의 이름을 [myButton]으로 작성한다

 
Option Explicit
Private WithEvents oButton As MSForms.CommandButton

Private Sub oButton_Click()
MsgBox "You Clicked Me!! My Caption Is [" & oButton.Caption & "] "
End Sub
Property Set SetButton(oX As MSForms.CommandButton)
Set oButton = oX
End Property
Property Get GetButton() As MSForms.CommandButton
Set GetButton = oButton
End Property

위에서 WithEvents 로 변수를 선언하면 해당개체의 이벤트목록을
사용할수 있게 된다
그중의 하나의 이벤트가 oButton_Clik() 인것이다
이 프로시져속의 것이 버튼을 크릭하면 실행되게 되는 것

이것을 일반모듈시트에서 아래와 같이 작성한다
즉 위의 크래스 myButton에서 개체를 100개 만들어내는 것이다
즉 버튼을 100개 만드는 것


Dim oBtns(0 To 99) As myButton

Sub createCommandButton()
On Error Resume Next
Dim shtX As Worksheet
Dim oOLE As OLEObject
Dim rTarget As Excel.Range
Dim oQ As MSForms.CommandButton
Dim iZ As Integer, iX As Integer, iY As Integer
Application.DisplayAlerts = False
Worksheets("MySheet").Delete
Application.DisplayAlerts = True

Set shtX = Worksheets.Add
shtX.Name = "MySheet"
shtX.Columns.ColumnWidth = 2.88
shtX.Rows.RowHeight = 21

For iX = 1 To 10
    For iY = 1 To 10
        Set rTarget = shtX.Cells(iY + 3, iX + 3)
        With rTarget
            Set oOLE = shtX.OLEObjects.Add(ClassType:="Forms.CommandButton.1", Left:=.Left, Top:=.Top, Width:=.Width, Height:=.Height)
            Set oQ = oOLE.Object
            oQ.Caption = Chr(Int(Rnd() * 25) + 65)
            oQ.Font.Size = 8
            oQ.Font.Name = "tahoma"
            DoEvents
            Set oBtns(iZ) = New myButton
            Set oBtns(iZ).SetButton = oQ
            iZ = iZ + 1
        End With
    Next
Next
End Sub

UserForm의 CommadButton은 엑셀입장에서 보면 외계인이다
즉 ActiveX 개체인것이다, 엑셀자체의 개체가 아니다
그런 외계인을 시트에 삽입할때는 메뉴의 개체삽입의 OLEObject라는 개체인것이다
OLEObject는 시트에 삽입할수 있는 엑셀외의 것들을 OLEObject라고 하는 개체속에
구성이 되는 셈이다
CommandButton이라는 개체가 OLEObject라는 엑셀개체속에 쏙들어가있다고 보면 된다
그래서 CommandButton개체에 접근하려면 OLEObject.Object 라는 속성을
통하여 접근한다

아래 엑셀화일에 시트에서 실행하기와 UserForm에서 실행하기 두가지
내용이 구현되었다, 살펴보시고
이것을 어떻게 VSTO AddIn에서 처리를 하는지 관련되어서 계속하도록 하자
아하..VBA로 워크시트에서 한 내용은 안되고 VSTO로 하니까되네..!!

***[LOG-IN]***

위의 VBA쌤플을 해보시고, VSTO에서 처리를 하면
우선 버튼을 그냥 만들면 심심하니까..100개의 버튼중에 하나의 버튼이 있는
범위의 셀에 X표시를 하고 크릭할때마다 X가 있는 버튼인지 확인하는
보물찾기 버튼으로 만들어 보도록 하자



VB.Net의 아주 중요한 개념을 하나 챙길수 있다
개체를 생성할때 New 메소드에 매개변수를 전달한다는 점
VBA의 Class 모듈에서 만드는 개체는 New clsX 할때 ,매개변수를
전달할수 없다, 물론 속성을 만들어서 전달하면 되겠지만
VB.Net의 New메소드에 매개변수를 마음대로 전달할수 있다는 점이
흥미로운 점이고, 이것을 잘 활용하여야 한다
이것을 메소드의 OverLoading 이라고 하는 완벽한 개체지향프로그램의 특징이다
앞의 다른 코너에서도 많이 이야기 했지만, OverLoading이라는 개념을 챙기시고
New 메소드는 특별히 개체를 만드는 메소드이므로 Constructor 라고 부른다
아래와 같이 크래스모듈을 하나 추가하고 작성한다

Public Class myButton
    WithEvents btnX As Microsoft.Vbe.Interop.Forms.CommandButton
    Dim rRange As Excel.Range
    Sub New(ByVal bQ As Microsoft.Vbe.Interop.Forms.CommandButton, ByVal rTarget As Excel.Range)
        btnX = bQ
        rRange = rTarget
    End Sub
    Private Sub btnX_Click() Handles btnX.Click
        If Globals.ThisAddIn.bFinish Then
            Exit Sub
        End If
        If rRange.Value = "X" Then
            btnX.Font.Bold = True
            btnX.BackColor = 3
            MsgBox(Format(rRange.Worksheet.Range("A1").Value / 100, "0.00%"))
            Globals.ThisAddIn.bFinish = True
        Else
            btnX.Caption = ""
        End If
        rRange.Worksheet.Range("A1").Value += 1
    End Sub
End Class

VBA에서는 크래스모듈이름이 clsButton이라고 하면

Dim oButton As clsButton
Set oButton=New clsButton

이라고 매개변수를 전달하는 형식으로 되지 않지만 VB.Net의 모든 크래스는
개체를 만들때

Dim oButton As clsButton
oButton=New clsButton(매개변수, 매개변수,,,필요한대로)

와 같이 매개변수를 필요에 따라서 크래스모듈에 설계를 해 놓고 사용할수 있는 것이다
또한 크래스모듈에서 New()를 설계할때

Sub New()
Sub New(iX As Integer)
Sub New(sQ As String)
Sub New(sQ As String,iX As Integer)

와 같이 같은 이름으로 다양한 종류의 매개변수를 전달하기도 하고, 하지 않기도하고
편리하게 사용할수 있다는 점이 메소드의 OverLoading 방식이라는 것을 다시 알고 계시고..

그래서 위의 크래스모듈에서는 매개변수를 버튼개체와 버튼이 위치한 셀 즉 Range개체를
전달하게 한 것이다
그러면 버튼이 크릭하면서 자신이 갖고 있는 범위를 간단하게 확인할수 있게 되는 셈이다

그런데 전략적으로 좀 생각해 볼 문제가 있다
만약 버튼 크릭이벤트에서 실행하는 내용이 아주 복잡하고 길다면
어떻게 하는 것이 좋을까???
크래스모듈은 버튼의 갯수만큼 만들어진다
만약 버튼을 100개 만들면 크래스모듈도 100개의 개체로 만들어지는 것이다
그렇다면 버튼이 크릭할때 실행되는 코드도 당연히 100개가 만들어지는 셈이다
만약 어떤 똑같은 일을 하는 공장이 동네마다 만들어진다고 하면 참으로
비경제적일 것이다
공장을 하나만 만들고 각 동네에서 필요할때 공장에 주문을 하는 것이
사람이라면 생각해 볼일인 것이다
그래서 크래스모듈내에서는 중요한 연결작업만 하고
일반모듈시트나 다른 개체에 메소드를 하나 만들어 놓고
100개의 각각의 버튼이 크릭될때 외부의 메소드를 호출하는 방식이
좋을 것이다..
사람이 살면서 하는 짓이나 , 컴퓨터속의 짓이나 항상 같다는 점을
잊지 않는 것이 효율적인 프로그래밍을 할 수 있는 것
우선 위와 같이 크래스모듈에 때려 넣은 방법으로 한 것을 보시기 바란다

***[LOG-IN]***