Запись макроса (меню Excel "Сервис\Макрос\Начать запись…") незаменимая вещь при написании отчетов или создания диаграмм в Excel'е, особенно для тех, кто только начинает с ним работать. Но, записанный в Excel макрос, иногда выглядит довольно громоздко и читается с трудом. В данной статье я хочу рассмотреть методы перевода записанных макросов в более удобный вид для использования их в Delphi. Также будет рассмотрены некоторые нестыковки в объектной модели Excel'я в записанных макросах и методы их исправления.
Для начала рассмотрим записанные в Excel'е макросы и попробуем сократить их VBA-код для переноса в Delphi. Откроем в Excel'e новую книгу и выполним, к примеру, простые действия - запустим запись макроса, выделим область "A1:D5" и в тулбаре "Границы" выберем "Все границы". Остановим запись макроса и посмотрим, что у нас получилось. Должен появиться примерно такой код (чтоб открыть VBA редактор в Excel'е нажмите Alt+F11):
Sub Макрос1()
'
Range("A1:D5").Select
Selection.Borders(xlDiagonalDown).LineStyle = xlNone
Selection.Borders(xlDiagonalUp).LineStyle = xlNone
With Selection.Borders(xlEdgeLeft)
.LineStyle = xlContinuous
.Weight = xlThin
.ColorIndex = xlAutomatic
End With
With Selection.Borders(xlEdgeTop)
.LineStyle = xlContinuous
.Weight = xlThin
.ColorIndex = xlAutomatic
End With
With Selection.Borders(xlEdgeBottom)
.LineStyle = xlContinuous
.Weight = xlThin
.ColorIndex = xlAutomatic
End With
With Selection.Borders(xlEdgeRight)
.LineStyle = xlContinuous
.Weight = xlThin
.ColorIndex = xlAutomatic
End With
With Selection.Borders(xlInsideVertical)
.LineStyle = xlContinuous
.Weight = xlThin
.ColorIndex = xlAutomatic
End With
With Selection.Borders(xlInsideHorizontal)
.LineStyle = xlContinuous
.Weight = xlThin
.ColorIndex = xlAutomatic
End With
End Sub
Да, многовато… Давайте посмотрим, что содержит полученный VBA-код:
- Выделили область и убрали диагональные линии (а они у нас были?).
- Нарисовали последовательно левую, верхнюю, правую, нижнюю границы.
- Нарисовали внутренние горизонтальные и вертикальные границы.
Теперь попробуем сократить этот макрос, например, так (скопируйте код, приведенный ниже в VBA редактор):
Sub Макрос1_1()
'
With Range("A1:D5").Borders
.LineStyle = xlContinuous
.Weight = xlThin
.ColorIndex = xlAutomatic
End With
End Sub
Очистим область "A1:D5" от границ и запустим наш макрос (перейдите в Excel из редактора, нажмите Alt+F8, выберите Макрос1_1 и нажмите "Выполнить"). Код намного короче, а результат тот же! Что мы сделали? Во-первых, убрали Select, просто указав какую область мы будем "обордюривать", во-вторых, вообще не указали какие границы будем заполнять, просто написав Borders без параметров (т.е. все). Почему понадобилось убирать Select? Потому что, во-первых, можно обойтись без него, а во-вторых, Select вызывает доп. перерисовку экрана, а это, как известно, самые долгие операции.
Теперь перейдем к другой "особенности" записи макроса, а именно к непонятному свойству объекта [Excel.]Application Selection. Что это такое? В данном макросе, как можно догадаться это область ячеек (Range). Давайте запишем еще один макрос: добавим окно инструментов "Рисование", включим запись макроса, выберем тулбар "Надпись", поместим ее на наш лист и наберем текст "Наша надпись". Выделим ячейку A1 и остановим запись макроса. Должен получиться примерно такой код:
Sub Макрос2()
'
ActiveSheet.Shapes.AddTextbox(msoTextOrientationHorizontal, 19.5, 88.5, _
191.25, 86.25).Select
Selection.Characters.Text = "Наша надпись"
With Selection.Characters(Start:=1, Length:=7).Font
.Name = "Arial"
.FontStyle = "обычный"
.Size = 10
.Strikethrough = False
.Superscript = False
.Subscript = False
.OutlineFont = False
.Shadow = False
.Underline = xlUnderlineStyleNone
.ColorIndex = xlAutomatic
End With
Range("A1").Select
End Sub
Опять попробуем сократить код:
Sub Макрос2_2()
Dim MyShape As Shape
Set MyShape = ActiveSheet.Shapes.AddTextbox(msoTextOrientationHorizontal, _
19.5, 88.5, 191.25, 86.25)
MyShape.Characters.Text = "Наша надпись"
End Sub
Перейдем в Excel, удалим нашу надпись и выполним макрос Макрос2_2.
Получим ошибку "Объект не поддерживает данное свойство или метод" на
строке с кодом
MyShape.Characters.Text = "Наша надпись".
Почему Selection его поддерживает, а Shape нет?
Посмотрев на объект Shape мы не найдем свойства
Characters.
Что же скрывается за загадочным Selection?
Для того чтобы это понять давайте в Макрос2,
добавим строку MsgBox TypeName(Selection) после строки
Selection.Characters.Text = "Наша надпись"
и выполним макрос. Получим сообщение
"TextBox".
Вот оно что! Значит Selection - это TextBox. Попробуем создать
такой объект и… Нет такого объекта! Есть только TextFrame. Замена Shape на
TextFrame тоже не увенчается успехом… Что же делать?
Посмотрим на свойства
объекта Shape и увидим там свойство TextFrame, у которого уже есть свойство
Characters… Посмотрев справку по VBA можно убедиться, что Characters -
это метод и принадлежит объекту TextFrame. Пробуем:
Sub Макрос2_2()
'
Dim MyShape As Shape
Set MyShape = ActiveSheet.Shapes.AddTextbox(msoTextOrientationHorizontal, _
19.5, 88.5, 191.25, 86.25)
MyShape.TextFrame.Characters.Text = "Наша надпись"
End Sub
Запустим макрос - работает! Оставим мифический TextBox на совести Microsoft…
Примечание:
объект TextBox таки существует, но только как Control для Form.
Еще небольшой пример на VBA про Selection и займемся непосредственно переносом кода из VBA в Delphi. Откройте файл Книга1.xls, который приложен к статье и перейдите на Лист2. Там таблица и график. Включим запись макроса, выделим первый столбик, вызовем "Формат рядов данных" и изменим цвет на темно синий. Остановим запись. Должен получиться примерно такой код:
Sub Макрос3()
'
ActiveSheet.ChartObjects("Диагр. 1").Activate
ActiveChart.SeriesCollection(1).Select
With Selection.Border
.Weight = xlThin
.LineStyle = xlAutomatic
End With
Selection.InvertIfNegative = False
With Selection.Interior
.ColorIndex = 23
.Pattern = xlSolid
End With
End Sub
Проверим, как он работает - перейдем в Excel, вызовем макросы и запустим Макрос3… Ошибка на первой же строке! Записанный макрос не работает. Почему? Попробуем сделать так, чтоб он заработал. Напишем небольшой макрос (руками) и будем вставлять в него код и тестировать. Начнем с определения имен имеющихся на листе диаграмм:
Sub Test1()
Dim i As Integer
For i = 1 To ActiveSheet.ChartObjects.Count
MsgBox ActiveSheet.ChartObjects(i).Name
Next i
End Sub
Запустив макрос, получим имя диаграммы "Chart 1" - почему не "Диагр. 1", как в записанном макросе - это очередная загадка. Исправим макрос и проверим:
Sub Макрос3()
'
ActiveSheet.ChartObjects("Chart 1").Activate
ActiveChart.SeriesCollection(1).Select
With Selection.Border
.Weight = xlThin
.LineStyle = xlAutomatic
End With
Selection.InvertIfNegative = False
With Selection.Interior
.ColorIndex = 23
.Pattern = xlSolid
End With
End Sub
Работает :o).
Дальше определим тип объекта после строки ActiveChart.SeriesCollection(1).Select известной строкой MsgBox TypeName(Selection). Получим Series. Сократим макрос и избавимся от Selection.
Sub Макрос3_3()
'
Dim ch As Chart, s As Series
Set ch = ActiveSheet.ChartObjects("Chart 1").Chart
Set s = ch.SeriesCollection(1)
With s.Interior
.ColorIndex = 23
.Pattern = xlSolid
End With
End Sub
Если посмотреть на код Макрос3 и Макрос3_3, то видно, что код в Макрос3 использует Selection как промежуточный буфер для передачи управления между объектами, т.е. Activate, Select и для "безликого" вызова свойств и методов. Чтобы получить объект типа Chart нам понадобилось добавить обращение к свойству ChartObject.Chart
Set ch = ActiveSheet.ChartObjects("Chart 1").Chart
Дальше мы просто поменяли цвет столбика без использования Select.
Конечно, это далеко не все загадки при записи макросов —
их еще много, но нам сейчас нужно было понять, что это возможно и как с
этим бороться.
Перенесем наш код в Delphi и параллельно в C# (если не возражаете).
Сразу оговорюсь, что в статье не рассматриваются методы подключения к
Excel'ю (по данному вопросу можно почитать
здесь ), также используется
раннее связывание (что это такое читайте здесь).
Я считаю позднее связывание не "паскалевким" подходом, так как везде
используется один тип Variant (как в языке "Основняк"),
что, по моему, сродни шаманизму — что-то происходит, что-то куда то записывается,
но никто не понимает, почему это работает.
Начнем с Макрос1. Да, именно с него, а не сокращенного варианта. Попытаемся написать код для первых трех строк:
Delphi
ASheet.Range['A1:D5', EmptyParam].Select;
XL.Selection[lcid].Borders[xlDiagonalDown].LineStyle := xlNone;
XL.Selection[lcid].Borders[xlDiagonalUp].LineStyle := xlNone;
Попробовав скомпилировать данный участок, сразу же получим ошибку компилятора "E2003 Undeclared identifier: 'Borders'". Посмотрим, какой тип имеет Selection (в данном примере смотрим файл Excel2000.pas):
property ExcelApplication.Selection[lcid: Integer]: IDispatch;
Посмотрев на интерфейс IDispatch, мы в самом деле не найдем такого свойства и метода... Попробуем подправить код:
Delphi
ASheet.Range['A1:D5', EmptyParam].Select;
(XL.Selection[lcid] As ExcelRange).Borders[xlDiagonalDown].LineStyle := xlNone;
(XL.Selection[lcid] As ExcelRange).Borders[xlDiagonalUp].LineStyle := xlNone;
With (XL.Selection[lcid] As ExcelRange).Borders[xlEdgeLeft] do begin
LineStyle := xlContinuous;
Weight := xlThin;
ColorIndex := xlAutomatic;
End;
With (XL.Selection[lcid] As ExcelRange).Borders[xlEdgeTop] do begin
LineStyle := xlContinuous;
Weight := xlThin;
ColorIndex := xlAutomatic;
End;
With (XL.Selection[lcid] As ExcelRange).Borders[xlEdgeBottom] do begin
LineStyle := xlContinuous;
Weight := xlThin;
ColorIndex := xlAutomatic;
End;
With (XL.Selection[lcid] As ExcelRange).Borders[xlEdgeRight] do begin
LineStyle := xlContinuous;
Weight := xlThin;
ColorIndex := xlAutomatic;
End;
C#
ASheet.get_Range("A1:D5", Type.Missing).Select();
((Excel.Range) XL.Selection).Borders.get_Item(
Excel.XlBordersIndex.xlDiagonalDown).LineStyle =
Excel.XlLineStyle.xlLineStyleNone;
((Excel.Range) XL.Selection).Borders.get_Item(
Excel.XlBordersIndex.xlDiagonalUp).LineStyle =
Excel.XlLineStyle.xlLineStyleNone;
// левая граница
((Excel.Range) XL.Selection).Borders.get_Item(
Excel.XlBordersIndex.xlEdgeLeft).LineStyle =
Excel.XlLineStyle.xlContinuous;
((Excel.Range) XL.Selection).Borders.get_Item(
Excel.XlBordersIndex.xlEdgeLeft).Weight =
Excel.XlBorderWeight.xlThin;
((Excel.Range) XL.Selection).Borders.get_Item(
Excel.XlBordersIndex.xlEdgeLeft).ColorIndex =
Excel.XlColorIndex.xlColorIndexAutomatic;
// верхняя граница
((Excel.Range) XL.Selection).Borders.get_Item(
Excel.XlBordersIndex.xlEdgeTop).LineStyle =
Excel.XlLineStyle.xlContinuous;
((Excel.Range) XL.Selection).Borders.get_Item(
Excel.XlBordersIndex.xlEdgeTop).Weight =
Excel.XlBorderWeight.xlThin;
((Excel.Range) XL.Selection).Borders.get_Item(
Excel.XlBordersIndex.xlEdgeTop).ColorIndex =
Excel.XlColorIndex.xlColorIndexAutomatic;
// нижняя граница
((Excel.Range) XL.Selection).Borders.get_Item(
Excel.XlBordersIndex.xlEdgeBottom).LineStyle =
Excel.XlLineStyle.xlContinuous;
((Excel.Range) XL.Selection).Borders.get_Item(
Excel.XlBordersIndex.xlEdgeBottom).Weight =
Excel.XlBorderWeight.xlThin;
((Excel.Range) XL.Selection).Borders.get_Item(
Excel.XlBordersIndex.xlEdgeBottom).ColorIndex =
Excel.XlColorIndex.xlColorIndexAutomatic;
// правая граница
((Excel.Range) XL.Selection).Borders.get_Item(
Excel.XlBordersIndex.xlEdgeRight).LineStyle =
Excel.XlLineStyle.xlContinuous;
((Excel.Range) XL.Selection).Borders.get_Item(
Excel.XlBordersIndex.xlEdgeRight).Weight =
Excel.XlBorderWeight.xlThin;
((Excel.Range) XL.Selection).Borders.get_Item(
Excel.XlBordersIndex.xlEdgeRight).ColorIndex =
Excel.XlColorIndex.xlColorIndexAutomatic;
Работает… Что мы для этого сделали? Привели тип IDispatch к ExcelRange: XL.Selection[lcid] as ExcelRange). Но такой перевод записанного макроса в Delphi поистине героический труд, да и нужен ли нам Select для того чтоб нарисовать границы (а глядя на C# код, вообще можно сразу отказаться на нем программировать)? Ведь всякая перерисовка - лишняя трата времени и, следовательно, скорости. Поэтому займемся Макросом1_1:
Delphi
With ASheet.Range['A1:D5', EmptyParam].Borders do begin
LineStyle := xlContinuous;
Weight := xlThin;
ColorIndex := xlAutomatic;
End;
C#
oRng = ASheet.get_Range("A1:D5", Type.Missing);
// установим две границы
oRng.Borders.LineStyle = Excel.XlLineStyle.xlContinuous;
oRng.Borders.Weight = Excel.XlBorderWeight.xlThin;
oRng.Borders.ColorIndex = Excel.XlColorIndex.xlColorIndexAutomatic;
Различия есть? Мы не делали Select и не использовали безликий Selection, обратившись непосредственно к области ExcelRange. Или все же лучше с Selection? Сравните:
Delphi
ASheet.Range['A1:D5', EmptyParam].Select;
With (XL.Selection[lcid] As ExcelRange).Borders do begin
LineStyle := xlContinuous;
Weight := xlThin;
ColorIndex := xlAutomatic;
End;
Все то же самое, но что-то рябит в глазах при Select, не правда ли? И вроде как-то медленнее или мне показалось?
Перейдем к Макрос2, вернее к уже подготовленному Макрос2_2:
Delphi
MyShape := (XL.ActiveWorkbook.ActiveSheet As _Worksheet).Shapes.AddTextbox(
msoTextOrientationHorizontal, 19.5, 88.5, 191.25, 86.25);
MyShape.TextFrame.Characters(EmptyParam, EmptyParam).Text := 'Наша надпись';
C#
myShape = (Excel.Shape) ASheet.Shapes.AddTextbox(
Office.MsoTextOrientation.msoTextOrientationHorizontal,
(float) 19.5, (float) 88.5, (float) 191.25, (float) 86.25);
myShape.TextFrame.Characters(Type.Missing, Type.Missing).Text =
"Наша надпись";
В коде на Delphi практически никаких отличий, кроме указания двух обязательных параметров: начала изменяемых символов и их длины. Мы написали EmptyParam, тем самым указав, что обрабатывается весь текст.
И, наконец, Макрос3_3. Усложним его - полностью создадим таблицу с данными, создадим график и изменим цвет первого столбца:
Delphi
oSheet.Cells.Item[1, 1] := 'First Name';
oSheet.Cells.Item[1, 2] := 'Last Name';
oSheet.Cells.Item[1, 3] := 'Full Name';
oSheet.Cells.Item[1, 4] := 'Salary';
//Format A1:D1 as bold, vertical alignment := center.
oSheet.Range['A1', 'D1'].Font.Bold := True;
oSheet.Range['A1', 'D1'].VerticalAlignment := xlVAlignCenter;
// Create an array to multiple values at once.
saNames := VarArrayCreate([0, 4, 0, 1], varVariant);
saNames[0, 0] := 'John';
saNames[0, 1] := 'Smith';
saNames[1, 0] := 'Tom';
saNames[1, 1] := 'Brown';
saNames[2, 0] := 'Sue';
saNames[2, 1] := 'Thomas';
saNames[3, 0] := 'Jane';
saNames[3, 1] := 'Jones';
saNames[4, 0] := 'Adam';
saNames[4, 1] := 'Johnson';
oSheet.Range['A2', 'B6'].Formula := saNames;
oRng := oSheet.Range['C2', 'C6'];
oRng.Formula := '=A2 & " " & B2';
oRng := oSheet.Range['D2', 'D6'];
oRng.Formula := '=RAND()*100000';
oSheet.Range['A1', 'D1'].EntireColumn.AutoFit;
// создадим график на листе в обласи E8:L29
Ch := (oSheet.ChartObjects As ChartObjects).Add(
oSheet.Range['B8', EmptyParam].Left,
oSheet.Range['B8', EmptyParam].Top,
oSheet.Range['I8', EmptyParam].Left - oSheet.Range['B8', EmptyParam].Left,
oSheet.Range['B30', EmptyParam].Top - oSheet.Range['B8', EmptyParam].Top).Chart
As _Chart;
oRng := oSheet.Range['C1', 'D6'];
With Ch do begin
SetSourceData(oRng, xlRows);
ChartType := xl3DColumnClustered;
HasTitle[lcid] := True;
ChartTitle[lcid].Characters[EmptyParam, EmptyParam].Text := 'Диаграмма 1';
(Axes(xlCategory, xlPrimary, lcid) As Axis).HasTitle := False;
(Axes(xlValue, xlPrimary, lcid) As Axis).HasTitle := True;
(Axes(xlValue, xlPrimary, lcid) As Axis).AxisTitle.
Characters[EmptyParam, EmptyParam].Text := 'Деньги';
(Axes(xlValue, xlPrimary, lcid) As Axis).AxisTitle.Orientation := xlUpward;
End;
// здесь код замены цвета у первого столбика
// взятый из Макрос3_3
With (Ch.SeriesCollection(1, lcid) As Series) do begin
Interior.ColorIndex := 23;
Interior.Pattern := xlSolid;
End;
C#
oSheet.Cells[1, 1] = "First Name";
oSheet.Cells[1, 2] = "Last Name";
oSheet.Cells[1, 3] = "Full Name";
oSheet.Cells[1, 4] = "Salary";
//Format A1:D1 as bold, vertical alignment := center.
oSheet.get_Range("A1", "D1").Font.Bold = true;
oSheet.get_Range("A1", "D1").VerticalAlignment =
Excel.XlVAlign.xlVAlignCenter;
oSheet.get_Range("A1", "D1").HorizontalAlignment =
Excel.XlHAlign.xlHAlignCenter;
// Create an array to multiple values at once.
string[,] saNames = new string[5, 2];
saNames[0, 0] = "John";
saNames[0, 1] = "Smith";
saNames[1, 0] = "Tom";
saNames[1, 1] = "Brown";
saNames[2, 0] = "Sue";
saNames[2, 1] = "Thomas";
saNames[3, 0] = "Jane";
saNames[3, 1] = "Jones";
saNames[4, 0] = "Adam";
saNames[4, 1] = "Johnson";
oSheet.get_Range("A2", "B6").Formula = saNames;
//Fill C2:C6 with a relative formula (=A2 & " " & B2).
oRng = oSheet.get_Range("C2", "C6");
oRng.Formula = "=A2 & \" \" & B2";
//Fill D2:D6 with a formula(=RAND()*100000) and apply format.
oRng = oSheet.get_Range("D2", "D6");
// oRng.Formula = "=RAND()*100000";
oRng.Formula = "=СЛЧИС()*100000";
// oRng.NumberFormat = "0.00";
//AutoFit columns A:D.
oRng = oSheet.get_Range("A1", "D1");
oRng.EntireColumn.AutoFit();
// создадим график на листе в обласи E8:L29
Ch = ((Excel.ChartObjects) oSheet.ChartObjects(Type.Missing)).Add(
(double) oSheet.get_Range("B8", Type.Missing).Left,
(double) oSheet.get_Range("B8", Type.Missing).Top,
(double) oSheet.get_Range("I8", Type.Missing).Left -
(double) oSheet.get_Range("B8", Type.Missing).Left,
(double) oSheet.get_Range("B30", Type.Missing).Top -
(double) oSheet.get_Range("B8", Type.Missing).Top
).Chart;
oRng = oSheet.get_Range("C1", "D6");
Ch.SetSourceData(oRng, Excel.XlRowCol.xlRows);
Ch.ChartType = Excel.XlChartType.xl3DColumnClustered;
Ch.HasTitle = true;
Ch.ChartTitle.get_Characters(Type.Missing, Type.Missing).Text = "Диаграмма 1";
((Excel.Axis) Ch.Axes(Excel.XlAxisType.xlCategory,
Excel.XlAxisGroup.xlPrimary)).HasTitle = false;
((Excel.Axis) Ch.Axes(Excel.XlAxisType.xlValue,
Excel.XlAxisGroup.xlPrimary)).HasTitle = true;
((Excel.Axis) Ch.Axes(Excel.XlAxisType.xlValue,
Excel.XlAxisGroup.xlPrimary)).AxisTitle.
get_Characters(Type.Missing, Type.Missing).Text = "Деньги";
((Excel.Axis) Ch.Axes(Excel.XlAxisType.xlValue,
Excel.XlAxisGroup.xlPrimary)).AxisTitle.Orientation =
Excel.XlOrientation.xlUpward;
// здесь код замены цвета у первого столбика
// взятый из Макрос3_3
((Excel.Series) Ch.SeriesCollection(1)).Interior.ColorIndex = 23;
((Excel.Series) Ch.SeriesCollection(1)).Interior.Pattern =
Excel.XlPattern.xlPatternSolid;
Из перенесенных строк из Макрос3_3 видно, что коллекция Ch.SeriesCollection(1, lcid) тоже возвращает интерфейс IDispatch, поэтому мы привели ее к типу Series. Почему в библиотеке типов сразу не использован тип Series остается только гадать. Еще в только что описанном примере приведен код задания титулов для осей (axes) и здесь метаморфоза превращения Axes в Axis, т.е. Axes - это коллекция Axis, хотя в VBA это ни как не отображается.
Резюме:
Мы рассмотрели несколько примеров перевода VBA кода, созданного записью макроса в Excel в Delphi. Увидели, как можно сократить ненужный код, избавившись от Select. Как уйти от безликого Selection (тип IDispatch) во избежание ошибок и возможных недоразумений. Также обнаружили несоответствие записанного кода (к примеру, имени объекта "Наша надпись") и типов реальным типам объектов. Т.е. записанный код VBA не всегда оказывается работоспособным. Для правильного перевода VBA в Delphi требуется представление об объектной модели Excel'я, обращение к справке Excel VBA, а также большое желание достигнуть результата.
- Все примеры тестировались на BDS 2006 и Microsoft Office 2003
- К статье прилагается Книга1.xls с приведенными в статье макросами и Demo-проект на Delphi и C#. Для работы проекта на C# требуется Framework 1.1
Полезные ссылки:
- MSDN:Automating Excel Using the Excel Object Model
- How to automate Microsoft Excel from Microsoft Visual C# .NET
- MSDN:Understanding the Excel Object Model from a .NET Developer's Perspective
- MSDN:Microsoft Excel Object Model
- Excel Tips: Macros and VBA
- Add a Chart to a Worksheet Programmatically (C#)
- MSDN: Best Practices for Setting Range Properties
Специально для Королевства Delphi
К материалу прилагаются файлы: