Question

I am developing an add-in for PowerPoint (2013) with C# / VSTO. The add-in will work when the user is in design mode, not presentation mode.

How can I catch mouse events with regard to shapes/objects on the slides, e.g. mouseOver, mouseDown etc? I want to listen to these events in order to create custom UI located near the objects / shapes. Are there any events I can listen to, or is it necessary to use more advanced methods, such as creating a global mouse listener, translate the coordinates to the PowerPoint shapes, and loop through the shapes on the slide to see whether the mouse is within the boundaries of any of the shapes? I will also appreciate other creative solutions to the problem.

I have tried to search for an answer without any luck. However, I know that it is somehow possible because other add-ins are doing what I want. One example is Think-Cell (https://www.youtube.com/watch?v=qsnciEZi5X0), where the objects you manipulate are "ordinary" PowerPoint objects such as TextFrames and Shapes.

I'm working with .Net 4.5 on Windows 8.1 Pro.

Was it helpful?

Solution

PowerPoint does not expose these events directly, but it is possible to implement your own events by combining global mouse hooks with shape parameters that PowerPoint expose.

This answer covers the more difficult case with MouseEnter and MouseLeave. To handle other events such as MouseDown and MouseUp, you can make use of the provided PowerPoint event Application_WindowSelectionChange as Steve Rindsberg proposed.

To get a global mouse hook, you can use excellent Application and Global Mouse and Keyboard Hooks .Net Libary in C# found at http://globalmousekeyhook.codeplex.com/

You will need to download and reference the library, and then you set up the mouse hook by using this code

// Initialize the global mouse hook
_mouseHookManager = new MouseHookListener(new AppHooker());
_mouseHookManager.Enabled = true;

// Listen to the mouse move event
_mouseHookManager.MouseMove += MouseHookManager_MouseMove;

The MouseMove event could be set up to handle the mouse events like this (example with only MouseEnter and MouseLeave)

private List<PPShape> _shapesEntered = new List<PPShape>();       
private List<PPShape> _shapesOnSlide = new List<PPShape>();

void MouseHookManager_MouseMove(object sender, System.Windows.Forms.MouseEventArgs e)
{
    // Temporary list holding active shapes (shapes with the mouse cursor within the shape)
    List<PPShape> activeShapes = new List<PPShape>();       

    // Loop through all shapes on the slide, and add active shapes to the list
    foreach (PPShapes in _shapesOnSlide)
    {
        if (MouseWithinShape(s, e))
        {
            activeShapes.Add(s);
        }
    }

    // Handle shape MouseEnter events
    // Select elements that are active but not currently in the shapesEntered list
    foreach (PPShape s in activeShapes)
    {
        if (!_shapesEntered.Contains(s))
        {
            // Raise your custom MouseEntered event
            s.OnMouseEntered();

            // Add the shape to the shapesEntered list
            _shapesEntered.Add(s);
        }
    }

    // Handle shape MouseLeave events
    // Remove elements that are in the shapes entered list, but no longer active
    HashSet<long> activeIds = new HashSet<long>(activeShapes.Select(s => s.Id));
    _shapesEntered.RemoveAll(s => {
        if (!activeIds.Contains(s.Id)) {
            // The mouse is no longer over the shape
            // Raise your custom MouseLeave event
            s.OnMouseLeave();

            // Remove the shape
            return true;
        }
        else
        {
            return false;
        }
    });
}

where the _shapesOnSlide is a list holding all the shapes on the current slide, _shapesEntered is a list with the shapes that have been entered but not yet left, and PPShape is a wrapper object for PowerPoint shapes (seen below)

The MouseWithinShape method tests whether the mouse is currently within a shape on the slide. It does so by translating the shapes X- and Y-coordinates (which PowerPoint exposes in Points unit) to screen coordinates, and the tests whether the mouse is within that bounding box

/// <summary>
/// Test whether the mouse is within a shape
/// </summary>
/// <param name="shape">The shape to test</param>
/// <param name="e">MouseEventArgs</param>
/// <returns>TRUE if the mouse is within the bounding box of the shape; FALSE otherwise</returns>
private bool MouseWithinShape(PPShape shape, System.Windows.Forms.MouseEventArgs e)
{
    // Fetch the bounding box of the shape
    RectangleF shapeRect = shape.Rectangle;

    // Transform to screen pixels
    Rectangle shapeRectInScreenPixels = PointsToScreenPixels(shapeRect);

    // Test whether the mouse is within the bounding box
    return shapeRectInScreenPixels.Contains(e.Location);
}

/// <summary>
/// Transforms a RectangleF with PowerPoint points to a Rectangle with screen pixels
/// </summary>
/// <param name="shapeRectangle">The Rectangle in PowerPoint point-units</param>
/// <returns>A Rectangle in screen pixel units</returns>
private Rectangle PointsToScreenPixels(RectangleF shapeRectangle)
{
    // Transform the points to screen pixels
    int x1 = pointsToScreenPixelsX(shapeRectangle.X);
    int y1 = pointsToScreenPixelsY(shapeRectangle.Y);
    int x2 = pointsToScreenPixelsX(shapeRectangle.X + shapeRectangle.Width);
    int y2 = pointsToScreenPixelsY(shapeRectangle.Y + shapeRectangle.Height);

    // Expand the bounding box with a standard padding
    // NOTE: PowerPoint transforms the mouse cursor when entering shapes before it actually
    // enters the shape. To account for that, add this extra 'padding'
    // Testing reveals that the current value (in PowerPoint 2013) is 6px
    x1 -= 6;
    x2 += 6;
    y1 -= 6;
    y2 += 6;

    // Return the rectangle in screen pixel units
    return new Rectangle(x1, y1, x2-x1, y2-y1);

}

/// <summary>
/// Transforms a PowerPoint point to screen pixels (in X-direction)
/// </summary>
/// <param name="point">The value of point to transform in PowerPoint point-units</param>
/// <returns>The screen position in screen pixel units</returns>
private int pointsToScreenPixelsX(float point)
{
    // TODO Handle multiple windows
    // NOTE: PresStatic is a reference to the PowerPoint presentation object
    return PresStatic.Windows[1].PointsToScreenPixelsX(point);
}

/// <summary>
/// Transforms a PowerPoint point to screen pixels (in Y-direction)
/// </summary>
/// <param name="point">The value of point to transform in PowerPoint point-units</param>
/// <returns>The screen position in screen pixel units</returns>
private int pointsToScreenPixelsY(float point)
{
    // TODO Handle multiple windows
    // NOTE: PresStatic is a reference to the PowerPoint presentation object
    return PresStatic.Windows[1].PointsToScreenPixelsY(point);
}

Finally we implement a custom PPShape object that exposes the events we want to listen to

using System.Drawing;
using Microsoft.Office.Interop.PowerPoint;
using System;

namespace PowerPointDynamicLink.PPObject
{
    class PPShape
    {
        #region Fields
        protected Shape shape;

        /// <summary>
        /// Get the PowerPoint Shape this object is based on
        /// </summary>
        public Shape Shape
        {
            get { return shape; }
        }

        protected long id;
        /// <summary>
        /// Get or set the Id of the Shape
        /// </summary>
        public long Id
        {
            get { return id; }
            set { id = value; }
        }

        protected string name;
        /// <summary>
        /// Get or set the name of the Shape
        /// </summary>
        public string Name
        {
            get { return name; }
            set { name = value; }
        }

        /// <summary>
        /// Gets the bounding box of the shape in PowerPoint Point-units
        /// </summary>
        public RectangleF Rectangle
        {
            get
            {
                RectangleF rect = new RectangleF
                {
                    X = shape.Left,
                    Y = shape.Top,
                    Width = shape.Width,
                    Height = shape.Height
                };

                return rect;
            }
        }
        #endregion

        #region Constructor
        /// <summary>
        /// Creates a new PPShape object
        /// </summary>
        /// <param name="shape">The PowerPoint shape this object is based on</param>
        public PPShape(Shape shape)
        {
            this.shape = shape;
            this.name = shape.Name;
            this.id = shape.Id;
        }
        #endregion

        #region Event handling
        #region MouseEntered
        /// <summary>
        /// An event that notifies listeners when the mouse has entered this shape
        /// </summary>
        public event EventHandler MouseEntered = delegate { };

        /// <summary>
        /// Raises an event telling listeners that the mouse has entered this shape
        /// </summary>
        internal void OnMouseEntered()
        {
            // Raise the event
            MouseEntered(this, new EventArgs());
        }
        #endregion

        #region MouseLeave
        /// <summary>
        /// An event that notifies listeners when the mouse has left this shape
        /// </summary>
        public event EventHandler MouseLeave = delegate { };

        /// <summary>
        /// Raises an event telling listeners that the mouse has left this shape
        /// </summary>
        internal void OnMouseLeave()
        {
            // Raise the event
            MouseLeave(this, new EventArgs());
        }
        #endregion
        #endregion
    }
}

To be completely exhaustive, there are multiple extra elements that should be handled that are not covered here. This includes things such as pausing the mouse hook when the PowerPoint window deactivates, handling multiple PowerPoint windows and multiple screens etc.

OTHER TIPS

I encountered the same problem a few weeks back. But instead of going deep into the Windows API programming for listening the mouse events, I used Excel.Chart. Unlike PowerPoint.Chart it gives a hell lot of mouse events to work with like

Chart.MouseUp, Chart.MouseOver, Chart.WindowBeforeDoubleClick, Chart.WindowBeforeRightClick, Chart.DragOver etc.

Most probably by now you have gone deep into the Windows API programming. Were you successful in listening to the mouse event? If yes, then how you did it?

Thanks :)

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top