Руководство по созданию Excel add-in для начинающих

Здравствуй, %username%!

Недавно мне пришлось по долгу службы писать небольшой Excel add-in. К слову, до этого мой опыт работы с MS Office из-под .NET сводился к простеньким утилитам, генерировавшим отчеты в Excel при помощи Office Primary Interop Assemblies. А уж когда контора купила лицензии на Aspose.NET, взаимодействие с офисом через COM было забыто аки страшный сон. Так вот, начав разбираться в теме, я был неприятно удивлен отсутствием вменяемых русскоязычных туториалов для начинающих. Теперь хочу заполнить этот пробел. В качестве примера мы создадим add-in, который по нажатию кнопки генерирует на текущем листе список дней текущего месяца и выделяет выходные дни. Заинтересовавшихся прошу под кат!

Вместо предисловия


Заранее хочу оговориться, что сам я не профессионал в области разработки под MS Office, и здесь возможно найдутся люди, которые раскритикуют мое решение в пух и прах. Что ж, я буду рад любой конструктивной критике. Так же эта статья не является исчерпывающим руководством по написанию Excel Add-ins, не ставит цели подробно описать архитектуру Excel Add-ins или содержимое пространства имен Microsoft.Office.Core. Это вводная статья, которая, как я надеюсь, поможет новичку сориентироваться в новой для него теме и проведет через процесс создания add-in’а, который делает что-то осмысленное. Итак, приступим!

Готовим солюшен


Откроем студию и создадим новый проект типа Excel 2010 Add-in. Назовем его SampleAddIn:



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



Единственный файл, который сгененрировала для нас студия – это ThisAddIn.cs:

public partial class ThisAddIn
{
        private void ThisAddIn_Startup(object sender, System.EventArgs e)
        {
        }

        private void ThisAddIn_Shutdown(object sender, System.EventArgs e)
        {
        }

        #region VSTO generated code

        /// <summary>
        /// Required method for Designer support - do not modify
        /// the contents of this method with the code editor.
        /// </summary>
        private void InternalStartup()
        {
            this.Startup += new System.EventHandler(ThisAddIn_Startup);
            this.Shutdown += new System.EventHandler(ThisAddIn_Shutdown);
        }
        
        #endregion
}


Здесь все просто – мы всего лишь регистрируем обработчики событий запуска и завершения нашего add-in’а. Сейчас, естественно, наш Add-in ничего не делает, но вы уже можете его запустить как обычный проект, по нажатию F5. Что особенно приятно, на этом этапе можно не заморачиваться развертыванием, т.к. студия сама запускает Excel с нужными настройками безопасности и подсовывает ему наш Add-in. Если все получилось, то при запуске Excel вы увидите такую надпись:



Рисуем интерфейс


Добавим в солюшен элемент типа Ribbon и назовем его SampleRibbon



В дизайнере добавим туда кнопку:



И зададим названия для вкладки, группы и кнопки:



А так же создадим обработчик нажатия кнопки:

public partial class SampleRibbon
{
        private void SampleRibbon_Load(object sender, RibbonUIEventArgs e)
        {
            
        }

        private void button1_Click(object sender, RibbonControlEventArgs e)
        {
            MessageBox.Show("Hello, world!");
        }
}


Попробуем запустить наш проект. Если вы все сделали правильно, то в Excel’е появилась новая вкладка с кнопкой, по нажатию на которую появляется окно с сообщением.

Ковыряем чуть глубже


На данный момент мы столкнулись с двумя проблемами: во-первых, если в классе ThisAddIn есть поле Application, дающее доступ к запущенному экземпляру Excel, то в классе Ribbon ничего подобного нет, а следовательно непонятно, как именно взаимодействовать с листами Excel’я. Во-вторых, как мы все знаем, смешение логики отображения и бизнес-логики является отличной заготовкой для хорошей порции спагетти в вашем коде. Поэтому изменим код, так чтобы избавиться от этих двух проблем, а заодно разберемся в «магии», которая встроила наш Ribbon в Excel без каких-либо действий с нашей стороны.

За создание пользовательских лент в Add-in’е отвечает метод CreateRibbonExtensibilityObject из класса AddInBase, от которого на самом деле унаследован наш класс ThisAddIn. Слава богу, этот метод был предусмотрительно сделан виртуальным, так что перегрузить его не составит проблем. Для начала добавим в класс ThisAddIn код:

protected override Microsoft.Office.Core.IRibbonExtensibility CreateRibbonExtensibilityObject()
{
        return null;
}


И запустим проект. Вуаля! Вкладка исчезла. Теперь изменим класс SampleRibbon:

public partial class SampleRibbon
{
        public event Action ButtonClicked;

        private void SampleRibbon_Load(object sender, RibbonUIEventArgs e)
        {
            
        }

        private void button1_Click(object sender, RibbonControlEventArgs e)
        {
            if (ButtonClicked != null)
                ButtonClicked();
        }
}


И класс ThisAddIn:

protected override Microsoft.Office.Core.IRibbonExtensibility CreateRibbonExtensibilityObject()
{
        var ribbon = new SampleRibbon();
        ribbon.ButtonClicked += ribbon_ButtonClicked;
        return Globals.Factory.GetRibbonFactory().CreateRibbonManager(new IRibbonExtension[] { ribbon });
}

private void ribbon_ButtonClicked()
{
         MessageBox.Show("Hello, world!");
}


Запустим проект. Если вы все сделали правильно, то вкладка снова появилась, а по нажатию кнопки, как и раньше выводится окно с сообщением.

Работаем с ячейками


Теперь пришло время для того, ради чего собственно и был затеян весь это сыр-бор – генерации таблицы. Для начала создадим пару вспомогательных методов для оформления ячеек:

public static void MarkBold(dynamic border)
{
        border.Weight = XlBorderWeight.xlMedium;
}


Этот метод выделяет границу ячейки жирным. Кстати, обратите внимание на тип аргумента border – dynamic – начиная с .NET 4.0 это очень удобный способ работы с COM-объектами! Если вдруг вы не в курсе, рекомендую ознакомиться, например, с этой статьей. Для нашего удобства создадим еще пару методов:

public enum Border
{
        Left = 1,
        Right = 2,
        Top = 3,
        Bottom = 4
 }

private void MarkCell(dynamic cell, DateTime day)
{
        if (day.DayOfWeek == DayOfWeek.Saturday)
        {
               MarkBold(cell.Borders[Border.Left]);
               MarkBold(cell.Borders[Border.Top]);
               MarkBold(cell.Borders[Border.Bottom]);
        }
        if (day.DayOfWeek == DayOfWeek.Sunday)
        {
               MarkBold(cell.Borders[Border.Right]);
               MarkBold(cell.Borders[Border.Top]);
               MarkBold(cell.Borders[Border.Bottom]);
        }
        cell.Columns[1].ColumnWidth = 4;
}


Теперь напишем код для получения списка дней месяца:

private static IEnumerable<DateTime> GetDaysOfCurrentMonth()
{
        var today = DateTime.Today;
        var dayIter = new DateTime(today.Year, today.Month, 1);
        while (dayIter.Month == today.Month)
        {
               yield return dayIter;
               dayIter = dayIter.AddDays(1);
         }
}


И сведем это все воедино:

private void ribbon_ButtonClicked()
{
        GenerateTable(Application.ActiveWorkbook.ActiveSheet, 1, 1);
}

private void GenerateTable(dynamic wSheet, int row, int column)
{
         var currentMonth = GetDaysOfCurrentMonth().ToArray();
         for (int i = 0; i < currentMonth.Length; i++)
         {
                wSheet.Cells[row, column + i] = currentMonth[i].Day;
                MarkCell(wSheet.Cells[row, column + i], currentMonth[i]);
         }
}


Если вы нигде не ошиблись, то запустив проект, и нажав на кнопку на нашей вкладке, вы увидите нечто подобное:



Вместо заключения


Вот и все, я надеюсь, после прочтения этой статьи вы получили базовые представления о создании Excel Add-ins под .NET. Для дальнейшего чтения могу порекомендовать, конечно же, MSDN и этот блог – он мне очень помог при изучении этой темы. Всем удачи и спасибо за внимание!
Share post

Similar posts

Comments 11

    +1
    Неужели нет более простого способа получить доступ к Excel.Application в классе SampleRibbon? Все-таки эта панель — часть Excel Add-ins.
      0
      На самом деле я сам был удивлен. Если посмотреть доку на MSDN, там есть подозрительные свойства Context, Parent и Tag, но лично у меня во время выполнения они все время были равны null.
      С другой стороны, это может быть пинком в сторону неких Best Practices, поскольку у меня в результате получились что-то вроде MVP, что само по себе неплохо.
      +1
      Я бы посоветова почитать книгу VSTO 3.0 for Microsoft Office programming. Хотя текущая версия под MS Office 2010, который на скриншотах, уже 4.0.
      Visual Studio Tools for Office значительно удобнее для создания надстроек, чем COM. А за статью спасибо!
        0
        Спасибо за рекомендацию, почитаю обязательно!
        Кстати, я вот может путаюсь в терминологии, но как я понимаю, VSTO — это как раз-таки инфраструктура для создания надстроек под .NET + боекомплект PIA для взаимодействия с COM-компонентами офиса. В смысле, что надстройка не создается целиком как COM-компонент, но от взаимодействия с COM мы все равно никуда не уходим?
        +1
        да, сам некогда ковырял VSTO для вывода фото человека и инфы в Outlook 2007.
        Инфа бралась из Exchange Server 2008.

        Да «вменяемых русскоязычных туториалов» действительно не было.
        Но штука класная, особенно в условиях корпоративного мира.

        А как Вы устанавливали Add-in? политиками?
          0
          Да, штука действительно неплохая, мне тоже понравилось.
          Нет, вручную, т.к. там машин, на которых это добро надо было развернуть, было совсем немного.
          0
          у меня не появляется ribbon (даже при использовании CreateRibbonExtensibilityObject) и виснет студия, сначала спрашивает копировать ли pfx файл — и при любом ответе зависает.
            0
            Все разобрался, похожу проблема в том, что проект находился не на диске c:
              0
              оказалось с этим не связано, в чем причина пока не понятно. сборка не подгружается из студии (запускается пустой эксель — в списке COM модулей модуля проекта нет). После третьего запуска студия намертво виснет.
                0
                В общем помогла очистка решения и построения заново (странно — так как я пробовал на двух новых проектах и везхде были проблемы с запуском dll из студии.
            0
            Я конечно тот еще некроман, публикую комментарий к посту 5и летней давности, но все же:
            На данный момент доступ к экземпляру класса Application возможен через Globals.ThisAddIn.Application

            p.s. в свое время оставлял в закладках ссылку на вашу статью, вот сейчас наступил тот случай, когда она пригодилась

            Only users with full accounts can post comments. Log in, please.