通过VSTO在PowerPoint设计师中捕获鼠标事件

4
我正在使用C# / VSTO为PowerPoint (2013)开发插件。插件将在用户处于设计模式而非演示模式时运行。
如何针对幻灯片上的形状/对象捕获鼠标事件(例如mouseOver、mouseDown等)?我想监听这些事件以创建自定义UI,位于对象/形状附近。是否有任何事件可以监听,或者必须使用更高级的方法,例如创建全局鼠标监听器,将坐标转换为PowerPoint形状,并循环遍历幻灯片上的形状,查看鼠标是否在任何形状的边界内?我也会感激其他创意解决方案。
我已经尝试搜索答案,但没有成功。但是,我知道这是有可能的,因为其他插件正在实现我想要的功能。一个例子是Think-Cell(https://www.youtube.com/watch?v=qsnciEZi5X0),您操作的对象是“普通”的PowerPoint对象,例如TextFrames和Shapes。
我正在Windows 8.1 Pro上使用.Net 4.5。

PowerPoint没有提供任何与鼠标相关的事件,但是对于某些目的,捕获当前选择更改时发生的Selection Change事件就足够了。 - Steve Rindsberg
1
@SteveRindsberg;WindowSelectionChange事件在功能方面可以帮我很大忙,例如mouseDown、mouseUp等。然而,它无法让我创建一个对mouseOver操作做出反应的UI(例如,当鼠标经过文本框时,在该文本框周围绘制一个矩形,该文本框由我的插件使用)。 - Gedde
不,这对此完全没有帮助。我怀疑你需要进行一些深入的Win API编程才能实现这一点。 - Steve Rindsberg
2个回答

6

PowerPoint并没有直接暴露这些事件,但是可以通过将全局鼠标钩子与PowerPoint公开的形状参数相结合来实现自己的事件。

本答案涵盖了处理MouseEnter和MouseLeave等其他事件更困难的情况。要处理MouseDown和MouseUp等其他事件,可以使用提供的PowerPoint事件Application_WindowSelectionChange,就像Steve Rindsberg所建议的那样。

要获取全局鼠标钩子,您可以使用在http://globalmousekeyhook.codeplex.com/找到的出色的C#“应用程序和全局鼠标键盘钩子”库。

您需要下载并引用该库,然后使用以下代码设置鼠标钩子:

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

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

可以通过以下方式设置MouseMove事件来处理鼠标事件(仅使用MouseEnter和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;
        }
    });
}

其中_shapesOnSlide是一个列表,包含当前幻灯片上的所有形状,_shapesEntered是一个列表,包含已输入但尚未离开的形状,PPShape是PowerPoint形状的包装对象(如下所示)。

MouseWithinShape方法测试鼠标是否在幻灯片上的形状内。它通过将形状的X和Y坐标(PowerPoint以点为单位公开)转换为屏幕坐标,并测试鼠标是否在该边界框内来实现。

/// <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);
}

最后,我们实现了一个自定义的PPShape对象,它公开了我们想要监听的事件。
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
    }
}

为了完全详尽,还需要处理多个额外元素,这些元素在此处没有涉及。这包括暂停鼠标钩子(mouse hook)当PowerPoint窗口失活时,处理多个PowerPoint窗口和多个屏幕等。

嗨,我现在正在使用你提到的同一个库来实现我的代码。但是这让我想到它是一个全局鼠标钩子,这意味着即使应用程序(PowerPoint)被最小化或在后台运行,它也会运行。 - Zunair Zubair
2
如果你使用 new AppHooker() 初始化 MouseHooker,那么事件只会在当前应用程序(PowerPoint)获得焦点时触发。如果你使用 new GlobalHooker() 初始化 MouseHooker,则它也会在应用程序(PowerPoint)最小化或在后台运行时运行。 - Gedde

1
我几周前遇到了同样的问题。但是,我使用了Excel.Chart而不是深入Windows API编程来监听鼠标事件。与PowerPoint.Chart不同,它提供了大量的鼠标事件,例如:

Chart.MouseUpChart.MouseOverChart.WindowBeforeDoubleClickChart.WindowBeforeRightClickChart.DragOver等。

很可能现在你已经深入研究了Windows API编程。你成功监听到鼠标事件了吗?如果是,那么你是如何做到的呢?

谢谢 :)


有趣 - 而且很好知道 - 但不幸的是,我并不是在处理图表本身,而是不同类型的形状(例如TextFrames),所以这并没有帮助。问题中提到ThinkCell只是为了说明它应该是可能的,因为我确信他们是插入常规的PowerPoint形状来绘制他们的图表,而不是使用Excel.Chart或PowerPoint.Chart。我很快会发布我的进展信息。 - Gedde
我已经添加了一个答案,描述了我最终实现这个的方式。 - Gedde
1
干得好。我使用了 PowerPoint.Chart 并创建了一个不可见的基础系列来使柱形上升。 - Zunair Zubair
稍后会尝试鼠标钩子的事情。谢谢提供的信息 :) - Zunair Zubair
嘿,抱歉打扰您。但是我遇到了一个问题。我已经实现了MouseHook,并且我的自定义菜单在双击和右键单击时显示。但是一旦菜单关闭,PowerPoint的默认菜单会弹出屏幕。如何禁用这些默认菜单?是否有类似于PowerPoint在其DoubleClick和RightClick事件中提供的Cancel属性以禁用默认的PowerPoint操作? - Zunair Zubair
1
听起来你最好开始一个新的问题。提供一些关于你正在使用的事件和如何显示你的自定义菜单的更多信息,这样就会更容易帮助你了。 - Gedde

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接