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
- 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";
[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
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 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.
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):
private void btnComAddIns_Click(object sender, RibbonControlEventArgs e)
{
Globals.ThisAddIn.Application.CommandBars.ExecuteMso("ComAddInsDialog");
}
private void btnVisualBasic_Click(object sender, RibbonControlEventArgs e)
{
Globals.ThisAddIn.Application.CommandBars.ExecuteMso("VisualBasic");
}
Globals.ThisAddIn.Application.CommandBars.ExecuteMso("MacroSecurity");
- To use the VBA
MsgBox
:
using Microsoft.VisualBasic;
Interaction.MsgBox (...)
History
- 11th October, 2022: Initial version