Make a Countdown Timer Add-in for Powerpoint - Part 2
A walkthrough 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
- First let's create a new project:
Please select "Powerpoint VSTO Add-in" project template, and C#, click "Next".
- Key in project name as "
CountDown
", keep the rest as default, then click "Create". - Below is the skeleton created by the system:
- Add Ribbon (Visual Designer):
Select "CountDown" Project in the Solution Explorer Pane, right click the mouse, on Pop up menu, select "Add\New Items".
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.
- Insert 8 buttons into the Ribbon:
- Customize 8 buttons:
- In the end, all the 8 buttons shall look like below:
- Add AboutBox:
- Customize AboutBox as per below:
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\""; } } }
- Add
frmDuration
: - Customize
frmDuration
as per below: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; } } }
- Add Utilities
static
Class:Add all the utilities functions in this class as
static
, so they can be used without declaration.Below are some code snippet of the
Utilities
class, please refer to the source code for full version.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; } } }
- Add
AddInUtilities
Class for COM interface: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); } } }
- Add last piece of COM interface code snippet into
ThisAddin
class: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.
- VBA in PPT to call function in VSTO Addin:
"
CountDown
" is asub
inserted in the PPT, and "CountDownEx
" is asub
defined in the VSTO Add-in."
ToggleSound
" is asub
inserted in the PPT, and "ToggleSoundEx
" is asub
defined in the VSTO Add-in.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
- Windows API used in C#:
[DllImport("winmm.dll", EntryPoint = "mciSendStringA")] private static extern long mciSendString(string lpstrCommand, string lpstrReturnString, long uReturnLength, long hwndCallback);
CreateObject
C# Version: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; }
- Invoke function from PPT Menu (Original Functions):
// 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");
- To use the VBA
MsgBox
:using Microsoft.VisualBasic; Interaction.MsgBox (...)
History
- 11th October, 2022: Initial version