Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / XML

Make a Countdown Timer Add-in for Powerpoint - Part 2

4.33/5 (3 votes)
14 Oct 2022CPOL2 min read 6K   154  
A walkthrough to create a VSTO CountDown Timer Add-in for Powerpoint
In this post, you will see how to use C# to create a VSTO CountDown Timer add-in for Powerpoint.

Download

Introduction

Recently, I wrote an article titled "Make a Countdown Timer Add-in for Powerpoint - Part 1". In Part 1, I used only VBA to create the add-in. Now in Part 2, I am going to use C# to create a VSTO add-in for Powerpoint.

Background

Visual Studio Tools for Office (VSTO) is a set of development tools available in the form of a Visual Studio add-in (project templates) and a runtime. It greatly simplifies the development process of Office Add-in. I am going to build the same CountDown Timer add-in with Visual Studio 2019 now.

Using the Code

  1. First let's create a new project:

    Please select "Powerpoint VSTO Add-in" project template, and C#, click "Next".

    Image 1

  2. Key in project name as "CountDown", keep the rest as default, then click "Create".

    Image 2

  3. Below is the skeleton created by the system:

    Image 3

  4. Add Ribbon (Visual Designer):

    Select "CountDown" Project in the Solution Explorer Pane, right click the mouse, on Pop up menu, select "Add\New Items".

    Image 4

    Select Add Ribbon (Visual Designer) and click Add.

    Note: Alternatively, you can also select Add Ribbon (XML) and click Add, XML has more features to play around, however there is no GUI for XML, personally I prefer Visual Designer.

  5. Insert 8 buttons into the Ribbon:

    Image 5

  6. Customize 8 buttons:

    Image 6

  7. In the end, all the 8 buttons shall look like below:

    Image 7

  8. Add AboutBox:

    Image 8

  9. Customize AboutBox as per below:

    Image 9

    C#
    using System.Collections.Generic;
    using System.ComponentModel;
    using System.Drawing;
    using System.Linq;
    using System.Reflection;
    using System.Threading.Tasks;
    using System.Windows.Forms;
    
    namespace CountDown
    {
        partial class frmAboutBox : Form
        {	    
            public frmAboutBox()
            {
                InitializeComponent();
                this.Text = String.Format("About {0}", AssemblyTitle);
                this.labelProductName.Text = AssemblyProduct;
                this.labelVersion.Text = String.Format("Version {0}", AssemblyVersion);
                this.labelCopyright.Text = AssemblyCopyright;
                this.labelCompanyName.Text = AssemblyCompany;
                this.textBoxDescription.Text = "This Utility is for user 
                            to add \"CountDown Timers\" in PPT slides.\n" +
                    "It allows users to add any number of timers with 
                     different preset duration.\n" + 
                    "How to use:\n" + 
                    " 1. Find \"CountDown Tab\", then click on 
                         \"Install CountDown\"\n" + 
                    " 2. Select a slide and click on \"Add Timer\"\n" + 
                    " 3. To play the timer, in \"Slide Show\" mode, 
                         click on the Timer, it will start to count down, 
                         click again it reset.\n" + 
                    " 4. To change the preset duration & TextEffect, 
                         select a Timer on a slide, then click on \"Edit Timer\"\n" + 
                    " 5. To delete a timer, select a Timer on a slide, 
                         then click on \"Del Timer\"";
            }
        }
    }
  10. Add frmDuration:

    Image 10

  11. Customize frmDuration as per below:

    Image 11

    C#
    using Microsoft.VisualBasic;
    using System;
    using System.IO;
    using System.Collections.Generic;
    using System.ComponentModel;
    using System.Data;
    using System.Drawing;
    using System.Linq;
    using System.Text;
    using System.Threading.Tasks;
    using System.Windows.Forms;
    
    namespace CountDown
    {
        public partial class frmDuration : Form
        {
            private int nDuration;
            public int Duration
            {
                get
                {
                    return nDuration;
                }
                set
                {
                    nDuration = value;
                    cboDuration.Text = value.ToString();
                }
            }
    
            private int nTextEffectIdx;
            public int TextEffectIdx
            {
                get
                {
                    return nTextEffectIdx;
                }
                set
                {
                    nTextEffectIdx = value;
                    cboTextEffect.Text = value.ToString();
                }
            }
    
            private string sSoundEffect;
            public string SoundEffect
            {
                get
                {
                    return sSoundEffect;
                }
                set
                {
                    sSoundEffect = value;
                    cboSoundEffect.Text = value;
                }
            }
    
            public frmDuration()
            {
                InitializeComponent();
    
                ResetComboBox(cboDuration);
                ResetComboBox(cboSoundEffect);
                ResetComboBox(cboTextEffect);
            }
    
            private void ResetComboBox(ComboBox oComboBox)
            {
                if (oComboBox.Name == "cboDuration")
                {
                    oComboBox.Items.Clear();
                    oComboBox.Items.Add("1");
                    oComboBox.Items.Add("2");
                    oComboBox.Items.Add("3");
                    oComboBox.Items.Add("4");
                    oComboBox.Items.Add("5");
                    oComboBox.Items.Add("10");
                    oComboBox.Items.Add("15");
                    oComboBox.Items.Add("30");
                    oComboBox.Items.Add("45");
                    oComboBox.Items.Add("60");
                    oComboBox.Items.Add("90");
                    oComboBox.Items.Add("120");
                    oComboBox.Items.Add("150");
                    oComboBox.Items.Add("180");
                    oComboBox.Items.Add("210");
                    oComboBox.Items.Add("240");
                    oComboBox.Items.Add("300");
                    oComboBox.SelectedIndex = 4;
                }
                else if (oComboBox.Name == "cboSoundEffect")
                {
                    oComboBox.Items.Clear();
                    oComboBox.Items.Add("None");
    
                    string sFileName;
                    string sExt;
                    string sFolderPath = "c:\\Windows\\Media\\";
                    foreach (string sPath in Directory.GetFiles(sFolderPath))
                    {
                        sFileName = Path.GetFileName(sPath);
                        sExt = Path.GetExtension(sPath).ToLower();
                        if (sExt == ".wav" || sExt == ".mid" || sExt == ".mp3")
                        {
                            if (Strings.InStr(sFileName, "Windows") == 0)
                            {
                                oComboBox.Items.Add(sFileName);
                            }
                        }
                    }
                    oComboBox.SelectedIndex = 0;
                }
                else if (oComboBox.Name == "cboTextEffect")
                {
                    int i;
                    oComboBox.Items.Clear();
                    for (i = 0; i <= 49; i++)
                        oComboBox.Items.Add(Strings.Format(i, "00"));
                    oComboBox.SelectedIndex = 29;
                    nTextEffectIdx = 29;
                }
            }
    
            private void btnOK_Click(object sender, EventArgs e)
            {
                bool bIsDurationValid = false;
                try
                {
                    int nNum =int.Parse(cboDuration.Text);
                    bIsDurationValid = true;
                }
                catch (Exception)
                {
                    bIsDurationValid = false;
                }
    
                if (bIsDurationValid & cboSoundEffect.Text != "" & 
                                       cboTextEffect.Text != "")
                {
                    nDuration = int.Parse(cboDuration.Text);
                    sSoundEffect = cboSoundEffect.Text;
                    nTextEffectIdx = int.Parse(cboTextEffect.Text);
                    this.DialogResult = DialogResult.OK;
                    this.Hide();
                }
                else
                {
                    string sErrMsg = "";
                    if (!bIsDurationValid)
                    {
                        sErrMsg += "Please select a valid duration" + 
                                    Constants.vbCrLf;
                    }
                    if (cboSoundEffect.Text == "")
                    {
                        sErrMsg += "Please select a SoundEffect" + Constants.vbCrLf;
                    }
                    if (cboTextEffect.Text == "")
                    {
                        sErrMsg += "Please select a TextEffect" + Constants.vbCrLf;
                    }
                    Interaction.MsgBox(sErrMsg);
                }
            }
    
            private void cboDuration_TextChanged(object sender, EventArgs e)
            {
                int nValue;
                if (int.TryParse(cboDuration.Text, out nValue))
                {
                    nDuration = nValue;
                }
            }
    
            private void cboSoundEffect_SelectionChangeCommitted
                    (object sender, EventArgs e)
            {
                if (this.cboSoundEffect.Text != "None")
                {
                    Utilities.ReloadMediaFile(this.cboSoundEffect.Text);
                    Utilities.StartPlayingMediaFile();
                    Interaction.MsgBox("Click to OK to stop playing sound effect");
                    Utilities.ReloadMediaFile(sSoundEffect);
                }
            }
    
            private void cboTextEffect_SelectedIndexChanged
                         (object sender, EventArgs e)
            {
                int nIdx = int.Parse(cboTextEffect.Text);
                Image oImage = ImageList1.Images[nIdx];
                picDisplay.Image = oImage;
            }
        }
    }
  12. Add Utilities static Class:

    Add all the utilities functions in this class as static, so they can be used without declaration.

    Image 12

    Below are some code snippet of the Utilities class, please refer to the source code for full version.

    C#
    using Microsoft.Office.Interop.PowerPoint;
    using Microsoft.Vbe.Interop;
    using Microsoft.VisualBasic;
    using System;
    using System.IO;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using System.Runtime.InteropServices;
    
    namespace CountDown
    {
        public static class Utilities
        {
            public const string m_sCountDownShapeName = "CountDown";
            public const string m_sCountDownInstPrjName = "CountDownAddinInstPrj";
    
            public const string sCountDownShapeName = "CountDown";
            public const string sCountDownSymbolShapeName = "CountDownSymbol";
            public const string sCountDownGroupName = "grpCountDown";
            public const string sCountDownInstPrjName = "CountDownAddinInstPrj";
            public const string sCountDownFontName = "Amasis MT Pro Black";
            public const string sCountDownSymbolFontName = 
                                "Segoe UI Emoji"; //"WingDings"
    
    
            [DllImport("winmm.dll", EntryPoint = "mciSendStringA")]
            private static extern long mciSendString
                    (string lpstrCommand, string lpstrReturnString, 
                     long uReturnLength, long hwndCallback);
    
            private static bool bSoundIsPlaying;
    
            public static bool IsAccess2VBOMTrusted()
            {
                bool bIsTrusted;
                string sName;
                try
                {
                    sName = Globals.ThisAddIn.Application.
                            ActivePresentation.VBProject.Name;
                    bIsTrusted = true;
    
                }
                catch (Exception)
                {
                    bIsTrusted = false;
                }
                return bIsTrusted;
            }
    
            public static bool IsCountDownInstalled()
            {
                return IsComponentExist("modCountDown");
            }
    
            public static bool IsProjectProtected()
            {
                bool bIsProtected = false;
                if (Globals.ThisAddIn.Application.VBE.
                ActiveVBProject.Protection == vbext_ProjectProtection.vbext_pp_locked)
                {
                    bIsProtected = true;
                }
                return bIsProtected;
            }
    
            public static bool IsComponentExist(string sModuleName)
            {
                bool bExist = false;
                foreach (VBComponent oComponent in Globals.ThisAddIn.
                         Application.VBE.ActiveVBProject.VBComponents)
                {
                    if (oComponent.Name == sModuleName)
                    {
                        bExist = true;
                        break;
                    }
                }
                return bExist;
            }
        }
    }
  13. Add AddInUtilities Class for COM interface:
    C#
    using Microsoft.Office.Interop.PowerPoint;
    using System.Runtime.InteropServices;
    namespace CountDown
    {
        [ComVisible(true)]
        public interface IAddInUtilities
        {
            void ToggleSoundEx(Shape oShapeSymbol);
            void CountDownEx(Shape oShape);
        }
        [ComVisible(true)]
        [ClassInterface(ClassInterfaceType.None)]
        public class AddInUtilities: IAddInUtilities
        {
            public void ToggleSoundEx(Shape oShapeSymbol)
            {
                Utilities.ToggleSoundEx(oShapeSymbol);
            }
            public void CountDownEx(Shape oShape)
            {
                Utilities.CountDownEx(oShape);
            }
        }
    }
  14. Add last piece of COM interface code snippet into ThisAddin class:
    C#
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using System.Xml.Linq;
    using PowerPoint = Microsoft.Office.Interop.PowerPoint;
    using Office = Microsoft.Office.Core;
    
    namespace CountDown
    {
        public partial class ThisAddIn
        {
            private AddInUtilities utilities;
            protected override object RequestComAddInAutomationService()
            {
                if (utilities == null)
                    utilities = new AddInUtilities();
    
                return utilities;
            }
    
            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
        }
    }

Points of Interest

VSTO Add-in is quite different from the VBA Add-in, besides the knowledge from Part 1, below are some additional learnings in Part 2.

  1. VBA in PPT to call function in VSTO Addin:

    "CountDown" is a sub inserted in the PPT, and "CountDownEx" is a sub defined in the VSTO Add-in.

    "ToggleSound" is a sub inserted in the PPT, and "ToggleSoundEx" is a sub defined in the VSTO Add-in.

    VBA
    Public Sub ToggleSound(oShapeSymbol As Shape)
        Dim oAddIn As COMAddIn
        Dim oAddinUtility As Object
        Set oAddIn = Application.COMAddIns("CountDown")
        Set oAddinUtility = oAddIn.Object
        oAddinUtility.ToggleSoundEx oShapeSymbol
        Set oAddinUtility = Nothing
        Set oAddIn = Nothing
    End Sub
    
    Public Sub CountDown(oShape As Shape)
        Dim oAddIn As COMAddIn
        Dim oAddinUtility As Object
        Set oAddIn = Application.COMAddIns("CountDown")
        Set oAddinUtility = oAddIn.Object
        oAddinUtility.CountDownEx oShape
        Set oAddinUtility = Nothing
        Set oAddIn = Nothing
    End Sub        
  2. Windows API used in C#:
    C#
    [DllImport("winmm.dll", EntryPoint = "mciSendStringA")]
    private static extern long mciSendString(string lpstrCommand,
    string lpstrReturnString, long uReturnLength, long hwndCallback);
    
  3. CreateObject C# Version:
    C#
    public static string GetBase64FromBytes(byte[] varBytes) {
        Type DomDocType = Type.GetTypeFromProgID("MSXML2.DomDocument");
        dynamic DomDocInst = Activator.CreateInstance(DomDocType);
        DomDocInst.DataType = "bin.base64";
        DomDocInst.nodeTypedValue = varBytes;
        return Strings.Replace
        (DomDocInst.Text, Constants.vbLf, Constants.vbCrLf);
    }
    
    public static byte[] GetBytesFromBase64(string varStr)
    {
        Type DomDocType = Type.GetTypeFromProgID("MSXML2.DomDocument");
        dynamic DomDocInst = Activator.CreateInstance(DomDocType);
        dynamic Elm = DomDocInst.createElement("b64");
        Elm.DataType = "bin.base64";
        Elm.Text = varStr;
        return Elm.nodeTypedValue;
    }
    
  4. Invoke function from PPT Menu (Original Functions):
    C#
    // Show ComAddinsDialog
    private void btnComAddIns_Click(object sender, RibbonControlEventArgs e)
    {
        Globals.ThisAddIn.Application.CommandBars.ExecuteMso("ComAddInsDialog");
    }
    
    // Show VisualBasic Editor
    private void btnVisualBasic_Click(object sender, RibbonControlEventArgs e)
    {
        Globals.ThisAddIn.Application.CommandBars.ExecuteMso("VisualBasic");
    }
    
    // Show MacroSecurity Dialog
    Globals.ThisAddIn.Application.CommandBars.ExecuteMso("MacroSecurity");
    
  5. To use the VBA MsgBox:
    C#
    using Microsoft.VisualBasic;
    Interaction.MsgBox (...)
    

History

  • 11th October, 2022: Initial version

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)