Как использовать Инструменты в Excel VBA

51

Я пытаюсь реализовать некоторые формы для инженерного проекта и абстрагировать его для некоторых общих функций, чтобы иметь обобщенную программу.

То, что я пытаюсь сделать, это иметь интерфейс cShape и иметь cRectangle и cCircle реализовать cShape

Мой код ниже:

cShape интерфейс

Option Explicit

Public Function getArea()
End Function

Public Function getInertiaX()
End Function

Public Function getInertiaY()
End Function

Public Function toString()
End Function

cRectangle класс

Option Explicit
Implements cShape

Public myLength As Double ''going to treat length as d
Public myWidth As Double ''going to treat width as b

Public Function getArea()
    getArea = myLength * myWidth
End Function

Public Function getInertiaX()
    getInertiaX = (myWidth) * (myLength ^ 3)
End Function

Public Function getInertiaY()
    getInertiaY = (myLength) * (myWidth ^ 3)
End Function

Public Function toString()
    toString = "This is a " & myWidth & " by " & myLength & " rectangle."
End Function

cCircle класс

Option Explicit
Implements cShape

Public myRadius As Double

Public Function getDiameter()
    getDiameter = 2 * myRadius
End Function

Public Function getArea()
    getArea = Application.WorksheetFunction.Pi() * (myRadius ^ 2)
End Function

''Inertia around the X axis
Public Function getInertiaX()
    getInertiaX = Application.WorksheetFunction.Pi() / 4 * (myRadius ^ 4)
End Function

''Inertia around the Y axis
''Ix = Iy in a circle, technically should use same function
Public Function getInertiaY()
    getInertiaY = Application.WorksheetFunction.Pi() / 4 * (myRadius ^ 4)
End Function

Public Function toString()
    toString = "This is a radius " & myRadius & " circle."
End Function

Проблема заключается в том, что всякий раз, когда я запускаю свои тестовые примеры, возникает следующая ошибка:

Ошибка компиляции:

Объектный модуль должен реализовать '~' для интерфейса '~'

  • 0
    @ L42 почему щедрость здесь? Не могли бы вы объяснить ваши требования?
Теги:
excel-vba
excel
interface

5 ответов

66
Лучший ответ

Это эзотерическая концепция ООП, и вам нужно еще немного сделать и понять, использовать пользовательскую коллекцию фигур.

Вы можете сначала просмотреть this answer, чтобы получить общее представление о классах и интерфейсах в VBA.


Следуйте инструкциям ниже.

Сначала откройте Блокнот и скопируйте вставьте код ниже

VERSION 1.0 CLASS
BEGIN
  MultiUse = -1
END
Attribute VB_Name = "ShapesCollection"
Attribute VB_GlobalNameSpace = False
Attribute VB_Creatable = False
Attribute VB_PredeclaredId = False
Attribute VB_Exposed = False
Option Explicit

Dim myCustomCollection As Collection

Private Sub Class_Initialize()
    Set myCustomCollection = New Collection
End Sub

Public Sub Class_Terminate()
    Set myCustomCollection = Nothing
End Sub

Public Sub Add(ByVal Item As Object)
    myCustomCollection.Add Item
End Sub

Public Sub AddShapes(ParamArray arr() As Variant)
    Dim v As Variant
    For Each v In arr
        myCustomCollection.Add v
    Next
End Sub

Public Sub Remove(index As Variant)
    myCustomCollection.Remove (index)
End Sub

Public Property Get Item(index As Long) As cShape
    Set Item = myCustomCollection.Item(index)
End Property

Public Property Get Count() As Long
    Count = myCustomCollection.Count
End Property

Public Property Get NewEnum() As IUnknown
    Attribute NewEnum.VB_UserMemId = -4
    Attribute NewEnum.VB_MemberFlags = "40"
    Set NewEnum = myCustomCollection.[_NewEnum]
End Property

Сохраните файл как ShapesCollection.cls на рабочем столе.

Убедитесь, что вы сохранили его с расширением *.cls, а не ShapesCollection.cls.txt

Теперь откройте файл Excel, перейдите в VBE ALT + F11 и щелкните правой кнопкой мыши в Project Explorer. В раскрывающемся меню выберите Import File и перейдите к файлу.

Изображение 3749

Примечание. Сначала вам нужно сохранить код в файле .cls, а затем импортировать его, потому что VBEditor не позволяет вам использовать атрибуты. Атрибуты позволяют указать член по умолчанию в итерации и использовать для каждого цикла в пользовательских классах сбора

Подробнее: 1, 2, 3, 4

Теперь вставьте 3 класса модулей. Переименуйте соответственно и скопируйте-вставьте код

cShape, это ваш интерфейс

Public Function GetArea() As Double
End Function

Public Function GetInertiaX() As Double
End Function

Public Function GetInertiaY() As Double
End Function

Public Function ToString() As String
End Function

cCircle

Option Explicit

Implements cShape

Public Radius As Double

Public Function GetDiameter() As Double
    GetDiameter = 2 * Radius
End Function

Public Function GetArea() As Double
    GetArea = Application.WorksheetFunction.Pi() * (Radius ^ 2)
End Function

''Inertia around the X axis
Public Function GetInertiaX() As Double
    GetInertiaX = Application.WorksheetFunction.Pi() / 4 * (Radius ^ 4)
End Function

''Inertia around the Y axis
''Ix = Iy in a circle, technically should use same function
Public Function GetInertiaY() As Double
    GetInertiaY = Application.WorksheetFunction.Pi() / 4 * (Radius ^ 4)
End Function

Public Function ToString() As String
    ToString = "This is a radius " & Radius & " circle."
End Function

'interface functions
Private Function cShape_getArea() As Double
    cShape_getArea = GetArea
End Function

Private Function cShape_getInertiaX() As Double
    cShape_getInertiaX = GetInertiaX
End Function

Private Function cShape_getInertiaY() As Double
    cShape_getInertiaY = GetInertiaY
End Function

Private Function cShape_toString() As String
    cShape_toString = ToString
End Function

CRectangle

Option Explicit

Implements cShape

Public Length As Double ''going to treat length as d
Public Width As Double ''going to treat width as b

Public Function GetArea() As Double
    GetArea = Length * Width
End Function

Public Function GetInertiaX() As Double
    GetInertiaX = (Width) * (Length ^ 3)
End Function

Public Function GetInertiaY() As Double
    GetInertiaY = (Length) * (Width ^ 3)
End Function

Public Function ToString() As String
    ToString = "This is a " & Width & " by " & Length & " rectangle."
End Function

' interface properties
Private Function cShape_getArea() As Double
    cShape_getArea = GetArea
End Function

Private Function cShape_getInertiaX() As Double
    cShape_getInertiaX = GetInertiaX
End Function

Private Function cShape_getInertiaY() As Double
    cShape_getInertiaY = GetInertiaY
End Function

Private Function cShape_toString() As String
    cShape_toString = ToString
End Function

Теперь вам нужно Insert стандартное Module и скопировать-вставить приведенный ниже код

Module1

Option Explicit

Sub Main()

    Dim shapes As ShapesCollection
    Set shapes = New ShapesCollection

    AddShapesTo shapes

    Dim iShape As cShape
    For Each iShape In shapes
        'If TypeOf iShape Is cCircle Then
            Debug.Print iShape.ToString, "Area: " & iShape.GetArea, "InertiaX: " & iShape.GetInertiaX, "InertiaY:" & iShape.GetInertiaY
        'End If
    Next

End Sub


Private Sub AddShapesTo(ByRef shapes As ShapesCollection)

    Dim c1 As New cCircle
    c1.Radius = 10.5

    Dim c2 As New cCircle
    c2.Radius = 78.265

    Dim r1 As New cRectangle
    r1.Length = 80.87
    r1.Width = 20.6

    Dim r2 As New cRectangle
    r2.Length = 12.14
    r2.Width = 40.74

    shapes.AddShapes c1, c2, r1, r2
End Sub

Запустите Main Sub и проверьте результаты в Immediate Window CTRL + G

Изображение 3750


Комментарии и пояснения:

В вашем модуле класса ShapesCollection есть два подмножества для добавления элементов в коллекцию.

Первый метод Public Sub Add(ByVal Item As Object) просто берет экземпляр класса и добавляет его в коллекцию. Вы можете использовать его в своем Module1, как это

Dim c1 As New cCircle
shapes.Add c1

Public Sub AddShapes(ParamArray arr() As Variant) позволяет вам добавлять несколько объектов одновременно, разделяя их запятой , тем же способом, что и AddShapes() Sub.

Это неплохой дизайн, чем добавление каждого объекта отдельно, но это зависит от того, к чему вы собираетесь пойти.

Обратите внимание, как я прокомментировал некоторый код в цикле

Dim iShape As cShape
For Each iShape In shapes
    'If TypeOf iShape Is cCircle Then
        Debug.Print iShape.ToString, "Area: " & iShape.GetArea, "InertiaX: " & iShape.GetInertiaX, "InertiaY:" & iShape.GetInertiaY
    'End If
Next

Если вы удалите комментарии из строк 'If и 'End If, вы сможете печатать только объекты cCircle. Это было бы очень полезно, если бы вы могли использовать делегатов в VBA, но вы не можете, поэтому я показал вам другой способ печати только одного типа объектов. Очевидно, вы можете изменить оператор If в соответствии с вашими потребностями или просто распечатать все объекты. Опять же, вам решать, как вы собираетесь обрабатывать свои данные:)

  • 1
    Это фантастика! Не могли бы вы немного объяснить, как IUnknown используется в текущем контексте? Из быстрого Google я не мог понять это легко. Кроме того, это myCustomCollection.Remove (index) вместо value в ShapesCollection.cls ? Кусок красоты!
  • 2
    О, спасибо @Ioannis, это была простая ошибка, я исправил это сейчас. IUnknown является частью библиотеки типов stdole2.tlb . Вы можете использовать OLE / COM Object Viewer, чтобы обнаружить реализацию. Более подробное IUnknown может стать новым отдельным вопросом :)
Показать ещё 8 комментариев
10

Вот несколько теоретических и практических вкладов в ответы на них, если люди приходят сюда, которые задаются вопросом, что такое инструменты/интерфейсы.

Как известно, VBA не поддерживает наследование, поэтому мы можем почти слепо использовать интерфейсы для реализации общих свойств/поведения в разных классах. Тем не менее, я считаю, что полезно описать, что представляет собой концептуальная разница между ними, чтобы понять, почему это имеет значение позже.

  • Наследование: определяет отношение is-a (квадрат - это форма);
  • Интерфейсы: определите обязательное отношение (типичным примером является интерфейс drawable, который предписывает, чтобы объект-получатель должен реализовать метод draw). Это означает, что классы, происходящие из разных корневых классов, могут реализовывать обычное поведение.

Наследование означает, что базовый класс (некоторый физический или концептуальный архетип) расширен, тогда как интерфейсы реализуют набор свойств/методов, которые определяют определенное поведение.
Таким образом, можно сказать, что Shape - это базовый класс, из которого наследуются все другие формы, которые могут реализовывать интерфейс drawable, чтобы сделать все формы доступными. Этот интерфейс будет заключаться в контракте, который гарантирует, что каждый Shape имеет метод draw, определяющий, как/где должна быть нарисована фигура: круг может или не может быть сделан иначе, чем квадрат.

класс IDrawable:

'IDrawable interface, defining what methods drawable objects have access to
Public Function draw()
End Function

Так как VBA не поддерживает наследование, мы автоматически вынуждены выбирать для создания интерфейса IShape, который гарантирует, что определенные свойства/поведение будут реализованы с помощью общих фигур (квадрат, круг и т.д.), вместо создания абстрактного базового слоя формы из которого мы можем распространяться.

класс IShape:

'Get the area of a shape
Public Function getArea() As Double
End Function

Часть, в которой мы попадаем в неприятности, - это когда мы хотим сделать каждую фигуру пригодной.
К сожалению, поскольку IShape является интерфейсом, а не базовым классом в VBA, мы не можем реализовать интерфейс drawable в базовом классе. Похоже, что VBA не позволяет нам использовать один интерфейс для другого; после проверки этого, компилятор, похоже, не обеспечивает желаемого поведения. Другими словами, мы не можем реализовать IDrawable внутри IShape и ожидать, что экземпляры IShape будут вынуждены реализовать методы IDrawable из-за этого.
Мы вынуждены реализовать этот интерфейс для каждого родового класса формы, который реализует интерфейс IShape, и, к счастью, VBA позволяет реализовать несколько интерфейсов.

класс cSquare:

Option Explicit

Implements iShape
Implements IDrawable

Private pWidth          As Double
Private pHeight         As Double
Private pPositionX      As Double
Private pPositionY      As Double

Public Function iShape_getArea() As Double
    getArea = pWidth * pHeight
End Function

Public Function IDrawable_draw()
    debug.print "Draw square method"
End Function

'Getters and setters

Следующая часть, которая следует сейчас, - это то, где типичное использование/преимущества интерфейса вступают в игру.

Пусть начнется наш код, написав factory, который возвращает новый квадрат. (Это всего лишь обходной путь нашей неспособности отправлять аргументы непосредственно конструктору):

модуль mFactory:

Public Function createSquare(width, height, x, y) As cSquare

    Dim square As New cSquare

    square.width = width
    square.height = height
    square.positionX = x
    square.positionY = y

    Set createSquare = square

End Function

Наш основной код будет использовать factory для создания нового квадрата:

Dim square          As cSquare

Set square = mFactory.createSquare(5, 5, 0, 0)

Когда вы смотрите на методы, которые у вас есть в вашем распоряжении, вы заметите, что логически получаете доступ ко всем методам, определенным в классе cSquare:

Изображение 3751

Мы увидим позже, почему это актуально.

Теперь вам следует задаться вопросом, что произойдет, если вы действительно хотите создать коллекцию доступных объектов. В вашем приложении могут содержаться объекты, которые не являются фигурами, но которые еще доступны. Теоретически, ничто не мешает вам иметь интерфейс IComputer, который можно рисовать (может быть, какой-то клипарт или что-то еще).
Причина, по которой вам может понадобиться коллекция доступных объектов, заключается в том, что вы можете отображать их в цикле в определенный момент жизненного цикла приложения.

В этом случае я напишу класс декоратора, который обертывает коллекцию (мы увидим, почему). class collDrawables:

Option Explicit

Private pSize As Integer
Private pDrawables As Collection

'constructor
Public Sub class_initialize()
    Set pDrawables = New Collection
End Sub

'Adds a drawable to the collection
Public Sub add(cDrawable As IDrawable)
    pDrawables.add cDrawable

    'Increase collection size
    pSize = pSize + 1

End Sub

Декоратор позволяет добавлять некоторые удобные методы, которые не могут быть предоставлены в коллекциях vba, но фактическая точка здесь заключается в том, что коллекция будет принимать только объекты, которые можно вынести (реализовать интерфейс IDrawable). Если мы попытаемся добавить объект, который не является допустимым, будет выбрано несоответствие типа (разрешены только допустимые объекты!).

Таким образом, мы могли бы запрограммировать цикл создания объектов с возможностью рисования. Разрешить непривлекательный объект в коллекции приведет к ошибке. Контур рендеринга может выглядеть так:

Опция Явная

Public Sub app()

    Dim obj             As IDrawable
    Dim square_1        As IDrawable
    Dim square_2        As IDrawable
    Dim computer        As IDrawable
    Dim person          as cPerson 'Not drawable(!) 
    Dim collRender      As New collDrawables

    Set square_1 = mFactory.createSquare(5, 5, 0, 0)
    Set square_2 = mFactory.createSquare(10, 5, 0, 0)
    Set computer = mFactory.createComputer(20, 20)

    collRender.add square_1
    collRender.add square_2
    collRender.add computer

    'This is the loop, we are sure that all objects are drawable! 
    For Each obj In collRender.getDrawables
        obj.draw
    Next obj

End Sub

Обратите внимание, что приведенный выше код добавляет много прозрачности: мы объявили объекты как IDrawable, что делает его прозрачным, что цикл никогда не завершится неудачно, так как метод draw доступен для всех объектов в коллекции.
Если мы попытаемся добавить Person в коллекцию, это вызовет рассогласование типа, если этот класс Person не реализовал интерфейс с возможностью рисования.

Но, пожалуй, самая важная причина, по которой объявление объекта как интерфейса важна, заключается в том, что мы хотим разоблачить методы, которые были определены в интерфейсе, а не те общедоступные методы, которые были определены на отдельных классах, как мы видели раньше.

Dim square_1        As IDrawable 

Изображение 3752

Мы не только уверены, что square_1 имеет метод draw, но также гарантирует, что будут обнаружены только методы, определенные IDrawable.
Для квадрата преимущество этого может быть не сразу понятным, но давайте взглянем на аналогию из рамки коллекций Java, которая намного яснее.

Представьте, что у вас есть общий интерфейс с именем IList, который определяет набор методов, применяемых для разных типов списков. Каждый тип списка представляет собой определенный класс, который реализует интерфейс IList, определяет их собственное поведение и, возможно, добавляет больше собственных методов сверху.

Мы объявляем список следующим образом:

dim myList as IList 'Declare as the interface! 

set myList = new ArrayList 'Implements the interface of IList only, ArrayList allows random (index-based) access 

В приведенном выше коде объявление списка как IList гарантирует, что вы не будете использовать методы, специфичные для ArrayList, а только те методы, которые заданы интерфейсом. Представьте, что вы объявили список следующим образом:

dim myList as ArrayList 'We don't want this

У вас будет доступ к общедоступным методам, определенным в классе ArrayList. Иногда это может быть желательно, но часто мы просто хотим использовать внутреннее поведение класса и не определяться общедоступными методами класса.
Преимущество становится ясным, если мы используем этот ArrayList еще 50 раз в нашем коде, и внезапно мы обнаруживаем, что нам лучше использовать LinkedList (который допускает специфическое внутреннее поведение, связанное с этим типом списка).

Если мы выполнили интерфейс, мы можем изменить строку:

set myList = new ArrayList

to:

set myList = new LinkedList 

и ни один из других кодов не сломается, так как интерфейс гарантирует, что контракт выполнен, т.е. используются только общедоступные методы, определенные в IList, поэтому различные типы списков с возможностью замены с течением времени.

Последняя вещь (возможно, менее известное поведение в VBA) заключается в том, что вы можете предоставить интерфейс по умолчанию

Мы можем определить интерфейс следующим образом:

IDrawable:

Public Function draw()
    Debug.Print "Draw interface method"
End Function

и класс, который также реализует метод draw:

cSquare:

implements IDrawable 
Public Function draw()
    Debug.Print "Draw square method" 
End Function

Мы можем переключаться между реализациями следующим образом:

Dim square_1        As IDrawable

Set square_1 = New IDrawable
square_1.draw 'Draw interface method
Set square_1 = New cSquare
square_1.draw 'Draw square method    

Это невозможно, если вы объявите переменную как cSquare.
Я не могу сразу придумать хороший пример, когда это может быть полезно, но технически это возможно, если вы его протестируете.

10

Есть два недокументированных дополнения о VBA и инструкции "Реализованные".

  • VBA не поддерживает символ undescore '_' в имени метода унаследованного интерфейса производного класса. F.E. он не будет компилировать код с таким методом, как cShape.get_area (проверен в Excel 2007): VBA выдаст ошибку компиляции выше для любого производного класса.

  • Если производный класс не реализует собственный метод, названный как в интерфейсе, VBA успешно компилирует код, но этот метод будет недоступен через переменную типа производного класса.

  • 0
    Объявления функций в классе интерфейса могут не содержать подчеркивания. Это должно быть выделено большими жирными буквами в документации.
8

Мы должны реализовать все методы интерфейса в классе, который он использует.

класс cCircle

Option Explicit
Implements cShape

Public myRadius As Double

Public Function getDiameter()
    getDiameter = 2 * myRadius
End Function

Public Function getArea()
    getArea = Application.WorksheetFunction.Pi() * (myRadius ^ 2)
End Function

''Inertia around the X axis
Public Function getInertiaX()
    getInertiaX = Application.WorksheetFunction.Pi() / 4 * (myRadius ^ 4)
End Function

''Inertia around the Y axis
''Ix = Iy in a circle, technically should use same function
Public Function getIntertiaY()
    getIntertiaY = Application.WorksheetFunction.Pi() / 4 * (myRadius ^ 4)
End Function

Public Function toString()
    toString = "This is a radius " & myRadius & " circle."
End Function

Private Function cShape_getArea() As Variant

End Function

Private Function cShape_getInertiaX() As Variant

End Function

Private Function cShape_getIntertiaY() As Variant

End Function

Private Function cShape_toString() As Variant

End Function

classRectangle Class

Option Explicit
Implements cShape

Public myLength As Double ''going to treat length as d
Public myWidth As Double ''going to treat width as b
Private getIntertiaX As Double

Public Function getArea()
    getArea = myLength * myWidth
End Function

Public Function getInertiaX()
    getIntertiaX = (myWidth) * (myLength ^ 3)
End Function

Public Function getIntertiaY()
    getIntertiaY = (myLength) * (myWidth ^ 3)
End Function

Public Function toString()
    toString = "This is a " & myWidth & " by " & myLength & " rectangle."
End Function

Private Function cShape_getArea() As Variant

End Function

Private Function cShape_getInertiaX() As Variant

End Function

Private Function cShape_getIntertiaY() As Variant

End Function

Private Function cShape_toString() As Variant

End Function

класс cShape

Option Explicit

Public Function getArea()
End Function

Public Function getInertiaX()
End Function

Public Function getIntertiaY()
End Function

Public Function toString()
End Function

Изображение 3753

0

Очень интересная статья, чтобы понять, почему и когда интерфейс может быть полезен! Но я думаю, что ваш последний пример о реализации по умолчанию неверен. Первый вызов метода draw метода square_1, созданный как IDrawable, правильно печатает результат, который вы даете, но второй вызов метода draw метода square_1, созданного как cSquare, неверен, ничего не печатается. Вступают в игру 3 разных метода:

IDrawable.cls:

Public Function draw()
    Debug.Print "Interface Draw method"
End Function

cSquare.cls:

Implements IDrawable

Public Function draw()
    Debug.Print "Class Draw method"
End Function

Public Function IDrawable_draw()
    Debug.Print "Interfaced Draw method"
End Function

Стандартный модуль:

Sub Main()
    Dim square_1 As Class6_Methods_IDrawable
    Set square_1 = New Class6_Methods_IDrawable
    Debug.Print "square_1 : ";
    square_1.draw

    Dim square_2 As Class6_Methods_cSquare
    Set square_2 = New Class6_Methods_cSquare
    Debug.Print "square_2 : ";
    square_2.draw 

    Dim square_3 As Class6_Methods_IDrawable
    Set square_3 = New Class6_Methods_cSquare
    Debug.Print "square_3 : ";
    square_3.draw
End Sub

Результаты в:

square_1 : Interface Draw method
square_2 : Class Draw method
square_3 : Interfaced Draw method
  • 0
    Что такое Class6_Methods_ ?
  • 0
    остатки моего модуля тестирования, это исправлено спасибо

Ещё вопросы

Сообщество Overcoder
Наверх
Меню