#region License // // InteractiveToolTip.cs // // Copyright (C) 2012-2013 Alex Taylor. All Rights Reserved. // // InteractiveToolTip is published under the terms of the Code Project Open License. // http://www.codeproject.com/info/cpol10.aspx // #endregion License using System; using System.ComponentModel; using System.Drawing; using System.Runtime.InteropServices; using System.Text; using System.Windows.Forms; namespace DelftTools.Controls.Swf.DataEditorGenerator { #region Usings #endregion Usings /// /// Represents a balloon-style which supports caller-supplied interactive content. /// /// /// /// InteractiveToolTip offers similar behaviour to the modal functionality of , but replaces the text-content /// with a , which may be used to provide either more complex formatting than is possible with , /// or to implement an interactive control model. /// /// /// The may be anything you like. Transparent s are supported. /// /// public partial class InteractiveToolTip : Component { #region Inner types /// /// Specifies the preferred position of the 's stem. /// public enum StemPosition { /// /// The stem should be positioned at the bottom-left corner of the balloon. /// BottomLeft, /// /// The stem should be positioned in the centre of the bottom edge of the balloon. /// BottomCentre, /// /// The stem should be positioned at the bottom-right corner of the balloon. /// BottomRight, /// /// The stem should be positioned at the top-left corner of the balloon. /// TopLeft, /// /// The stem should be positioned in the centre of the top edge of the balloon. /// TopCentre, /// /// The stem should be positioned at the top-right corner of the balloon. /// TopRight } private sealed class Win32 { public const string TOOLTIPS_CLASS = "tooltips_class32"; public const int TTS_ALWAYSTIP = 0x01; public const int TTS_NOFADE = 0x10; public const int TTS_NOANIMATE = 0x20; public const int TTS_BALLOON = 0x40; public const int TTF_IDISHWND = 0x0001; public const int TTF_CENTERTIP = 0x0002; public const int TTF_TRACK = 0x0020; public const int TTF_TRANSPARENT = 0x0100; public const int WM_SETFONT = 0x30; public const int WM_GETFONT = 0x31; public const int WM_PRINTCLIENT = 0x318; public const int WM_USER = 0x0400; public const int TTM_TRACKACTIVATE = WM_USER + 17; public const int TTM_TRACKPOSITION = WM_USER + 18; public const int TTM_SETMAXTIPWIDTH = WM_USER + 24; public const int TTM_GETBUBBLESIZE = WM_USER + 30; public const int TTM_ADDTOOL = WM_USER + 50; public const int TTM_DELTOOL = WM_USER + 51; public const int SWP_NOSIZE = 0x0001; public const int SWP_NOACTIVATE = 0x0010; public const int SWP_NOOWNERZORDER = 0x200; public static readonly IntPtr HWND_TOPMOST = new IntPtr(-1); [DllImport("User32", SetLastError = true)] public static extern int GetWindowRect(IntPtr hWnd, ref RECT lpRect); [DllImport("User32", SetLastError = true)] public static extern int SendMessage(IntPtr hWnd, int Msg, int wParam, ref TOOLINFO lParam); [DllImport("User32", SetLastError = true)] public static extern int SendMessage(IntPtr hWnd, int Msg, int wParam, out RECT lParam); [DllImport("User32", SetLastError = true)] public static extern int SendMessage(IntPtr hWnd, int Msg, int wParam, int lParam); [DllImport("User32", SetLastError = true)] public static extern bool SetWindowPos(IntPtr hWnd, IntPtr hWndInsertAfter, int X, int Y, int cx, int cy, int uFlags); [DllImport("User32", SetLastError = true)] public static extern IntPtr SetParent(IntPtr hWndChild, IntPtr hWndNewParent); [StructLayout(LayoutKind.Sequential)] public struct RECT { public readonly int left; public readonly int top; public readonly int right; public readonly int bottom; } [StructLayout(LayoutKind.Sequential)] public struct SIZE { public readonly int cx; public readonly int cy; } [StructLayout(LayoutKind.Sequential)] public struct TOOLINFO { public int cbSize; public int uFlags; public IntPtr hwnd; public IntPtr uId; public readonly RECT rect; public readonly IntPtr hinst; [MarshalAs(UnmanagedType.LPTStr)] public string lpszText; public readonly UInt32 lParam; } } private class ContentPanel : UserControl { private readonly IntPtr _toolTipHwnd; public ContentPanel(IntPtr toolTipHWnd) { _toolTipHwnd = toolTipHWnd; Win32.SetParent(Handle, toolTipHWnd); } protected override void OnPaintBackground(PaintEventArgs e) { // paint the balloon Win32.SendMessage(_toolTipHwnd, Win32.WM_PRINTCLIENT, (int) e.Graphics.GetHdc(), 0); } } private class ToolTipWindow : NativeWindow, IDisposable { #region Internals // the distance from the edge of the balloon to the edge of the stem private const int StemInset = 16; private static StringFormat StringFormat = new StringFormat(StringFormat.GenericTypographic) { FormatFlags = StringFormatFlags.MeasureTrailingSpaces }; private ContentPanel _contentPanel; private Win32.TOOLINFO _toolInfo; private bool _mouseOverToolTip; private Win32.TOOLINFO CreateTool(string contentSpacing, IWin32Window window, StemPosition stemPosition) { Win32.TOOLINFO ti = new Win32.TOOLINFO(); ti.cbSize = Marshal.SizeOf(ti); ti.uFlags = Win32.TTF_IDISHWND | Win32.TTF_TRACK | Win32.TTF_TRANSPARENT; ti.uId = window.Handle; ti.hwnd = window.Handle; ti.lpszText = contentSpacing; if (StemPosition.BottomCentre == stemPosition || StemPosition.TopCentre == stemPosition) { ti.uFlags |= Win32.TTF_CENTERTIP; } if (0 == Win32.SendMessage(Handle, Win32.TTM_ADDTOOL, 0, ref ti)) { throw new Exception(); } // enable multi-line text-layout Win32.SendMessage(Handle, Win32.TTM_SETMAXTIPWIDTH, 0, SystemInformation.MaxWindowTrackSize.Width); return ti; } private StemPosition AdjustStemPosition(StemPosition stemPosition, ref Rectangle toolTipBounds, ref Rectangle screenBounds) { if (toolTipBounds.Left < screenBounds.Left) { // the window is too close to the left edge of the display if (StemPosition.TopCentre == stemPosition || StemPosition.TopRight == stemPosition) { stemPosition = StemPosition.TopLeft; } else if (StemPosition.BottomCentre == stemPosition || StemPosition.BottomRight == stemPosition) { stemPosition = StemPosition.BottomLeft; } } else if (toolTipBounds.Right > screenBounds.Right) { // the window is too close to the right edge of the display if (StemPosition.TopCentre == stemPosition || StemPosition.TopLeft == stemPosition) { stemPosition = StemPosition.TopRight; } else if (StemPosition.BottomCentre == stemPosition || StemPosition.BottomLeft == stemPosition) { stemPosition = StemPosition.BottomRight; } } if (toolTipBounds.Top < screenBounds.Top) { // the window is too close to the top edge of the display switch (stemPosition) { case StemPosition.BottomLeft: stemPosition = StemPosition.TopLeft; break; case StemPosition.BottomCentre: stemPosition = StemPosition.TopCentre; break; case StemPosition.BottomRight: stemPosition = StemPosition.TopRight; break; } } else if (toolTipBounds.Bottom > screenBounds.Bottom) { // the window is too close to the bottom edge of the display switch (stemPosition) { case StemPosition.TopLeft: stemPosition = StemPosition.BottomLeft; break; case StemPosition.TopCentre: stemPosition = StemPosition.BottomCentre; break; case StemPosition.TopRight: stemPosition = StemPosition.BottomRight; break; } } return stemPosition; } private Rectangle CalculateToolTipLocation(string contentSpacing, IWin32Window window, int x, int y, StemPosition stemPosition) { Rectangle toolTipBounds = new Rectangle(); Size toolTipSize = GetToolTipWindowSize(contentSpacing); Win32.RECT windowBounds = new Win32.RECT(); Win32.GetWindowRect(window.Handle, ref windowBounds); x += windowBounds.left; if (StemPosition.TopLeft == stemPosition || StemPosition.BottomLeft == stemPosition) { toolTipBounds.X = x - StemInset; } else if (StemPosition.TopCentre == stemPosition || StemPosition.BottomCentre == stemPosition) { toolTipBounds.X = x - (toolTipSize.Width/2); } else { toolTipBounds.X = x - toolTipSize.Width + StemInset; } if (StemPosition.TopLeft == stemPosition || StemPosition.TopCentre == stemPosition || StemPosition.TopRight == stemPosition) { toolTipBounds.Y = windowBounds.bottom - y; } else { toolTipBounds.Y = y + windowBounds.top - toolTipSize.Height; } toolTipBounds.Width = toolTipSize.Width; toolTipBounds.Height = toolTipSize.Height; return toolTipBounds; } private Size GetToolTipWindowSize(string contentSpacing) { Win32.TOOLINFO ti = new Win32.TOOLINFO(); ti.cbSize = Marshal.SizeOf(ti); ti.uFlags = Win32.TTF_TRACK; ti.lpszText = contentSpacing; if (0 == Win32.SendMessage(Handle, Win32.TTM_ADDTOOL, 0, ref ti)) { throw new Exception(); } // enable multi-line text-layout Win32.SendMessage(Handle, Win32.TTM_SETMAXTIPWIDTH, 0, SystemInformation.MaxWindowTrackSize.Width); Win32.SendMessage(Handle, Win32.TTM_TRACKACTIVATE, 1, ref ti); Win32.RECT rect = new Win32.RECT(); Win32.GetWindowRect(Handle, ref rect); Win32.SendMessage(Handle, Win32.TTM_TRACKACTIVATE, 0, ref ti); Win32.SendMessage(Handle, Win32.TTM_DELTOOL, 0, ref ti); return new Size(rect.right - rect.left, rect.bottom - rect.top); } private void DoLayout(IWin32Window window, Control content, StemPosition stemPosition, ref Rectangle toolTipBounds) { int bubbleSize = Win32.SendMessage(Handle, Win32.TTM_GETBUBBLESIZE, 0, ref _toolInfo); int bubbleWidth = bubbleSize & 0xFFFF; int bubbleHeight = bubbleSize >> 16; // centre our content on the bubble-area of the tooltip content.Left = (bubbleWidth - content.Width)/2; if (StemPosition.BottomLeft == stemPosition || StemPosition.BottomCentre == stemPosition || StemPosition.BottomRight == stemPosition) { // stem is below the bubble content.Top = (bubbleHeight - content.Height)/2; } else { // stem is on top of the bubble int bubbleOffset = toolTipBounds.Height - bubbleHeight; content.Top = (bubbleHeight - content.Height)/2 + bubbleOffset; } _contentPanel = new ContentPanel(Handle); _contentPanel.Width = toolTipBounds.Width; _contentPanel.Height = toolTipBounds.Height; _contentPanel.Controls.Add(content); Win32.SetWindowPos(Handle, Win32.HWND_TOPMOST, toolTipBounds.X, toolTipBounds.Y, 0, 0, Win32.SWP_NOACTIVATE | Win32.SWP_NOSIZE | Win32.SWP_NOOWNERZORDER); } private string GetSizingText(Control content) { // we can't set the dimensions of the tooltip directly - they are controlled by the space required to render its // text-content - so we must fake a string with approximately the same dimensions when rendered as our content StringBuilder sb = new StringBuilder(); Graphics graphics = Graphics.FromHwnd(Handle); Font font = Font.FromHfont((IntPtr) Win32.SendMessage(Handle, Win32.WM_GETFONT, 0, 0)); // use a small font to improve precision font = new Font(font.FontFamily, 1.0f); Win32.SendMessage(Handle, Win32.WM_SETFONT, (int) font.ToHfont(), 1); Size size = TextRenderer.MeasureText(" ", font); int rows = (content.Height + size.Height - 1)/size.Height; for (int n = 0; n < rows; n++) { sb.Append("\r\n"); } size = TextRenderer.MeasureText(sb.ToString(), font); // pad the width out to match the spacing on the height so the border around the content is of roughly constant size int width = content.Width + size.Height - content.Height; // we can't do a simple 'how many columns' calculation here, as the text-renderer will apply kerning while (size.Width < width) { sb.Append(" "); size = TextRenderer.MeasureText(sb.ToString(), font); } return sb.ToString(); } #endregion Internals #region Constructor public ToolTipWindow(Control content, IWin32Window window, int x, int y, StemPosition stemPosition, bool useAnimation, bool useFading) { Window = window; CreateParams createParams = new CreateParams(); createParams.ClassName = Win32.TOOLTIPS_CLASS; createParams.Style = Win32.TTS_ALWAYSTIP | Win32.TTS_BALLOON; if (!useAnimation) { createParams.Style |= Win32.TTS_NOANIMATE; } if (!useFading) { createParams.Style |= Win32.TTS_NOFADE; } CreateHandle(createParams); // first, work out the actual stem-position: the supplied value is a hint, but may have to be changed if there isn't enough space to accomodate it string contentSpacing = GetSizingText(content); // this is where the caller would like us to be Rectangle toolTipBounds = CalculateToolTipLocation(contentSpacing, Window, x, y, stemPosition); Screen currentScreen = Screen.FromHandle(Window.Handle); Rectangle screenBounds = currentScreen.WorkingArea; stemPosition = AdjustStemPosition(stemPosition, ref toolTipBounds, ref screenBounds); // and this is where we'll actually end up toolTipBounds = CalculateToolTipLocation(contentSpacing, Window, x, y, stemPosition); toolTipBounds.X = Math.Max(0, toolTipBounds.X); toolTipBounds.Y = Math.Max(0, toolTipBounds.Y); // build the tooltip window _toolInfo = CreateTool(contentSpacing, Window, stemPosition); // initial position to force the stem into the correct orientation int initialX = screenBounds.X; int initialY = screenBounds.Y; if (StemPosition.TopLeft == stemPosition || StemPosition.BottomLeft == stemPosition) { initialX += StemInset; } else if (StemPosition.TopCentre == stemPosition || StemPosition.BottomCentre == stemPosition) { initialX += screenBounds.Width/2; } else { initialX += screenBounds.Width - StemInset; } if (StemPosition.BottomLeft == stemPosition || StemPosition.BottomCentre == stemPosition || StemPosition.BottomRight == stemPosition) { initialY += screenBounds.Height; } Win32.SendMessage(Handle, Win32.TTM_TRACKPOSITION, 0, (initialY << 16) | initialX); // and finally display it Win32.SendMessage(Handle, Win32.TTM_TRACKACTIVATE, 1, ref _toolInfo); DoLayout(Window, content, stemPosition, ref toolTipBounds); _contentPanel.MouseEnter += delegate(object sender, EventArgs e) { if (null != MouseEnter && !_mouseOverToolTip) { _mouseOverToolTip = true; MouseEnter(this, e); } }; _contentPanel.MouseLeave += delegate(object sender, EventArgs e) { // only send the event if the mouse has actually left the balloon and not simply moved from _contentPanel to the caller-supplied content if (null != MouseLeave && _mouseOverToolTip && null == _contentPanel.GetChildAtPoint(_contentPanel.PointToClient(Control.MousePosition))) { _mouseOverToolTip = false; MouseLeave(this, e); } }; } ~ToolTipWindow() { Dispose(false); } public void Dispose() { Dispose(true); } private void Dispose(bool disposing) { if (disposing) { Win32.SendMessage(Handle, Win32.TTM_TRACKACTIVATE, 0, ref _toolInfo); Win32.SendMessage(Handle, Win32.TTM_DELTOOL, 0, ref _toolInfo); if (null != _contentPanel) { _contentPanel.Controls.Clear(); _contentPanel.Dispose(); } DestroyHandle(); } } #endregion Constructor #region API public readonly IWin32Window Window; public event EventHandler MouseEnter; public event EventHandler MouseLeave; #endregion API } #endregion Inner types #region Internals private readonly Timer _durationTimer; private ToolTipWindow _currentToolTip; #endregion Internals #region Properties /// /// Gets or sets a value determining whether an animation effect should be used when displaying the . /// /// /// /// true if window animation should be used; otherwise, false. The default is true. /// /// public bool UseAnimation { get; set; } /// /// Gets or sets a value determining whether a fade effect should be used when displaying the . /// /// /// /// true if window fading should be used; otherwise, false. The default is true. /// /// public bool UseFading { get; set; } #endregion Properties #region Constructor /// /// Initializes a new instance of the class without a specified container. /// public InteractiveToolTip() { InitializeComponent(); UseAnimation = true; UseFading = true; _durationTimer = new Timer(); components.Add(_durationTimer); _durationTimer.Tick += delegate(object sender, EventArgs e) { Hide(); }; } /// /// Initializes a new instance of the class with a specified container. /// /// An that represents the container of the . public InteractiveToolTip(IContainer container) : this() { container.Add(this); } /// /// Releases unmanaged resources and performs other cleanup operations before the /// is reclaimed by garbage collection. /// ~InteractiveToolTip() { Dispose(false); } #endregion Constructor #region API /// /// Sets the ToolTip content associated with the specified control, and displays the . /// /// The content to be displayed in the . /// The to display the for. /// /// /// The is displayed until either one of the Show methods is called to display another , /// or is called. It will be drawn with the stem positioned at the bottom-left of the balloon, tip located at the top-left corner of /// . If there is insufficient space on the display for this, the stem and balloon may be repositioned to accomodate this. /// /// public void Show(Control content, IWin32Window window) { Show(content, window, StemPosition.BottomLeft); } /// /// Sets the ToolTip content associated with the specified control, and displays the with its stem in the specified position. /// /// The content to be displayed in the . /// The to display the for. /// The desired position for the stem of the balloon. /// /// /// The is displayed until either one of the Show methods is called to display another , /// or is called. It will be drawn with the stem positioned at the specified part of the balloon, tip located at either the top-left corner /// (if specifies the bottom edge) or the bottom-left corner (if specifies the top edge) of /// . If there is insufficient space on the display for this, the stem and balloon may be repositioned to accomodate this. /// /// public void Show(Control content, IWin32Window window, StemPosition stemPosition) { Show(content, window, 0, 0, stemPosition, 0); } /// /// Sets the ToolTip content associated with the specified control, and displays the at the specified relative position. /// /// The content to be displayed in the . /// The to display the for. /// A containing the offset, in pixels, relative to the upper-left corner of the associated control window, to display the . /// /// /// The is displayed until either one of the Show methods is called to display another , /// or is called. It will be drawn with the stem positioned at the bottom-left of the balloon, tip located at the specified position relative /// to the top-left corner of . If there is insufficient space on the display for this, the stem and balloon may be repositioned to /// accomodate this. /// /// public void Show(Control content, IWin32Window window, Point location) { Show(content, window, location.X, location.Y); } /// /// Sets the ToolTip content associated with the specified control, and displays the at the specified relative position. /// /// The content to be displayed in the . /// The to display the for. /// The horizontal offset, in pixels, relative to the upper-left corner of the associated control window, to display the . /// The vertical offset, in pixels, relative to the upper-left corner of the associated control window, to display the . /// /// /// The is displayed until either one of the Show methods is called to display another , /// or is called. It will be drawn with the stem positioned at the bottom-left of the balloon, tip located at the specified position relative /// to the top-left corner of . If there is insufficient space on the display for this, the stem and balloon may be repositioned to /// accomodate this. /// /// public void Show(Control content, IWin32Window window, int x, int y) { Show(content, window, x, y, StemPosition.BottomLeft, 0); } /// /// Sets the ToolTip content associated with the specified control, and displays the at the specified relative position. /// /// The content to be displayed in the . /// The to display the for. /// A containing the offset, in pixels, relative to the upper-left corner of the associated control window, to display the . /// The desired position for the stem of the balloon. /// The time in milliseconds for which the should be displayed, or zero for indefinite display. /// /// /// The is displayed until either one of the Show methods is called to display another , /// or is called, or the specified is exceeded. It will be drawn with the stem positioned at the specified part /// of the balloon, tip located at the specified position relative to the top-left corner of . If there is insufficient space on the /// display for this, the stem and balloon may be repositioned to accomodate this. /// /// /// If the mouse is moved over the , the duration timer will be halted. It will resume - with its original value - when the mouse /// leaves the again. /// /// public void Show(Control content, IWin32Window window, Point location, StemPosition stemPosition, int duration) { Show(content, window, location.X, location.Y, stemPosition, duration); } /// /// Sets the ToolTip content associated with the specified control, and displays the at the specified relative position. /// /// The content to be displayed in the . /// The to display the for. /// The horizontal offset, in pixels, relative to the upper-left corner of the associated control window, to display the . /// The vertical offset, in pixels, relative to the upper-left corner of the associated control window, to display the . /// The desired position for the stem of the balloon. /// The time in milliseconds for which the should be displayed, or zero for indefinite display. /// /// /// The is displayed until either one of the Show methods is called to display another , /// or is called, or the specified is exceeded. It will be drawn with the stem positioned at the specified part /// of the balloon, tip located at the specified position relative to the top-left corner of . If there is insufficient space on the /// display for this, the stem and balloon may be repositioned to accomodate this. /// /// /// If the mouse is moved over the , the duration timer will be halted. It will resume - with its original value - when the mouse /// leaves the again. /// /// public void Show(Control content, IWin32Window window, int x, int y, StemPosition stemPosition, int duration) { if (null == content || null == window) { throw new ArgumentNullException(); } Hide(); _currentToolTip = new ToolTipWindow(content, window, x, y, stemPosition, UseAnimation, UseFading); if (duration > 0) { _currentToolTip.MouseEnter += delegate(object sender, EventArgs e) { _durationTimer.Stop(); }; _currentToolTip.MouseLeave += delegate(object sender, EventArgs e) { if (duration > 0) { _durationTimer.Start(); } }; _durationTimer.Interval = duration; _durationTimer.Start(); } if (null != ToolTipShown) { ToolTipShown(this, new InteractiveToolTipEventArgs(window)); } } /// /// Hides the current . /// /// /// /// If the is not visible, the method just returns. /// /// public void Hide() { _durationTimer.Stop(); ToolTipWindow toolTip = _currentToolTip; IWin32Window window; if (null != toolTip) { _currentToolTip = null; window = toolTip.Window; toolTip.Dispose(); if (null != ToolTipHidden) { ToolTipHidden(this, new InteractiveToolTipEventArgs(window)); } } } /// /// Occurs when an is shown. /// public event InteractiveToolTipEventHandler ToolTipShown; /// /// Occurs when an is hidden. /// public event InteractiveToolTipEventHandler ToolTipHidden; #endregion API } /// /// Represents a method which is invoked when an is shown. /// /// The sender. /// The event-args instance containing the event data. public delegate void InteractiveToolTipEventHandler(object sender, InteractiveToolTipEventArgs e); /// /// Provides data for the and events. /// public class InteractiveToolTipEventArgs : EventArgs { /// /// Initializes a new instance of the class with the specified parameters. /// /// The window representing the content of the . public InteractiveToolTipEventArgs(IWin32Window window) : base() { Window = window; } /// /// Gets the window representing the content of the . /// public IWin32Window Window { get; private set; } } }