vbAccelerator - Contents of code file: PopupControl_PopupWindowHelper.cs

using System;
using System.Drawing;
using System.Runtime.InteropServices;
using System.Windows.Forms;

namespace vbAccelerator.Components.Controls
{

   #region Event Argument Classes
   /// <summary>
   /// Contains event information for a <see cref="PopupClosed"/> event.
   /// </summary>
   public class PopupClosedEventArgs : EventArgs
   {
      /// <summary>
      /// The popup form.
      /// </summary>
      private Form popup = null;

      /// <summary>
      /// Gets the popup form which is being closed.
      /// </summary>
      public Form Popup
      {
         get
         {
            return this.popup;
         }
      }

      /// <summary>
      /// Constructs a new instance of this class for the specified
      /// popup form.
      /// </summary>
      /// <param name="popup">Popup Form which is being closed.</param>
      public PopupClosedEventArgs(Form popup)
      {
         this.popup = popup;
      }
   }

   /// <summary>
   /// Arguments to a <see cref="PopupCancelEvent"/>.  Provides a
   /// reference to the popup form that is to be closed and 
   /// allows the operation to be cancelled.
   /// </summary>
   public class PopupCancelEventArgs : EventArgs
   {
      /// <summary>
      /// Whether to cancel the operation
      /// </summary>
      private bool cancel = false;
      /// <summary>
      /// Mouse down location
      /// </summary>
      private Point location;
      /// <summary>
      /// Popup form.
      /// </summary>
      private Form popup = null;

      /// <summary>
      /// Constructs a new instance of this class.
      /// </summary>
      /// <param name="popup">The popup form</param>
      /// <param name="location">The mouse location, if any, where the
      /// mouse event that would cancel the popup occured.</param>
      public PopupCancelEventArgs(Form popup, Point location)
      {
         this.popup = popup;
         this.location = location;
         this.cancel = false;
      }

      /// <summary>
      /// Gets the popup form
      /// </summary>
      public Form Popup
      {
         get
         {
            return this.popup;
         }
      }

      /// <summary>
      /// Gets the location that the mouse down which would cancel this 
      /// popup occurred
      /// </summary>
      public Point CursorLocation
      {
         get
         {
            return this.location;
         }
      }

      /// <summary>
      /// Gets/sets whether to cancel closing the form. Set to
      /// <c>true</c> to prevent the popup from being closed.
      /// </summary>
      public bool Cancel
      {
         get
         {
            return this.cancel;
         }
         set
         {
            this.cancel = value;
         }
      }

   }
   #endregion

   #region Delegates
   /// <summary>
   /// Represents the method which responds to a <see cref="PopupClosed"/>
    event.
   /// </summary>
   public delegate void PopupClosedEventHandler(object sender,
    PopupClosedEventArgs e);

   /// <summary>
   /// Represents the method which responds to a <see cref="PopupCancel"/>
    event.
   /// </summary>
   public delegate void PopupCancelEventHandler(object sender,
    PopupCancelEventArgs e);
   #endregion

   #region PopupWindowHelper
   /// <summary>
   /// A class to assist in creating popup windows like Combo Box drop-downs
    and Menus.
   /// This class includes functionality to keep the title bar of the popup
    owner form
   /// active whilst the popup is displayed, and to automatically cancel the
    popup
   /// whenever the user clicks outside the popup window or shifts focus to
    another 
   /// application.
   /// </summary>
   public class PopupWindowHelper : NativeWindow
   {
      #region Unmanaged Code
      [DllImport("user32", CharSet = CharSet.Auto)]
      private extern static int SendMessage(IntPtr handle, int msg, int wParam,
       IntPtr lParam);

      [DllImport("user32", CharSet = CharSet.Auto)]
      private extern static int PostMessage(IntPtr handle, int msg, int wParam,
       IntPtr lParam);

      private const int WM_ACTIVATE = 0x006;
      private const int WM_ACTIVATEAPP = 0x01C;
      private const int WM_NCACTIVATE = 0x086;

      [DllImport("user32")]
      private extern static void keybd_event(byte bVk, byte bScan, int dwFlags,
       int dwExtraInfo);

      private const int KEYEVENTF_KEYUP       = 0x0002;
      #endregion

      #region Member Variables
      /// <summary>
      /// Event Handler to detect when the popup window is closed
      /// </summary>
      private EventHandler popClosedHandler = null;
      /// <summary>
      /// Message filter to detect mouse clicks anywhere in the application
      /// whilst the popup window is being displayed.
      /// </summary>
      private PopupWindowHelperMessageFilter filter = null;
      /// <summary>
      /// The popup form that is being shown.
      /// </summary>
      private Form popup = null;
      /// <summary>
      /// The owner of the popup form that is being shown:
      /// </summary>
      private Form owner = null;
      /// <summary>
      /// Whether the popup is showing or not.
      /// </summary>
      private bool popupShowing = false;
      /// <summary>
      /// Whether the popup has been cancelled, notified by PopupCancel,
      /// rather than closed.
      /// </summary>
      private bool skipClose = false;
      #endregion

      /// <summary>
      /// Raised when the popup form is closed.
      /// </summary>
      public event PopupClosedEventHandler PopupClosed;
      /// <summary>
      /// Raised when the Popup Window is about to be cancelled.  The
      /// <see cref="PopupCancelEventArgs.Cancel"/> property can be
      /// set to <c>true</c> to prevent the form from being cancelled.
      /// </summary>
      public event PopupCancelEventHandler PopupCancel;

      /// <summary>
      /// Shows the specified Form as a popup window, keeping the
      /// Owner's title bar active and preparing to cancel the popup
      /// should the user click anywhere outside the popup window.
      /// <para>Typical code to use this message is as follows:</para>
      /// <code>
      ///    frmPopup popup = new frmPopup();
      ///    Point location = this.PointToScreen(new Point(button1.Left,
       button1.Bottom));
      ///    popupHelper.ShowPopup(this, popup, location);
      /// </code>
      /// <para>Put as much initialisation code as possible
      /// into the popup form's constructor, rather than the <see
       cref="System.Windows.Forms.Load"/>
      /// event as this will improve visual appearance.</para>
      /// </summary>
      /// <param name="owner">Main form which owns the popup</param>
      /// <param name="popup">Window to show as a popup</param>
      /// <param name="location">Location relative to the screen to show the
       popup at.</param>
      public void ShowPopup(Form owner, Form popup, Point location)
      {

         this.owner = owner;
         this.popup = popup;

         // Start checking for the popup being cancelled
         Application.AddMessageFilter(filter);

         // Set the location of the popup form:
         popup.StartPosition = FormStartPosition.Manual;
         popup.Location = location; 
         // Make it owned by the window that's displaying it:
         owner.AddOwnedForm(popup);         
         // Respond to the Closed event in case the popup
         // is closed by its own internal means
         popClosedHandler = new EventHandler(popup_Closed);
         popup.Closed += popClosedHandler;

         // Show the popup:
         this.popupShowing = true;
         popup.Show();
         popup.Activate();
         
         // A little bit of fun.  We've shown the popup,
         // but because we've kept the main window's
         // title bar in focus the tab sequence isn't quite
         // right.  This can be fixed by sending a tab,
         // but that on its own would shift focus to the
         // second control in the form.  So send a tab,
         // followed by a reverse-tab.

         // Send a Tab command:
         keybd_event((byte) Keys.Tab, 0, 0, 0);
         keybd_event((byte) Keys.Tab, 0, KEYEVENTF_KEYUP, 0);

         // Send a reverse Tab command:
         keybd_event((byte) Keys.ShiftKey, 0, 0, 0);
         keybd_event((byte) Keys.Tab, 0, 0, 0);
         keybd_event((byte) Keys.Tab, 0, KEYEVENTF_KEYUP, 0);
         keybd_event((byte) Keys.ShiftKey, 0, KEYEVENTF_KEYUP, 0);


         // Start filtering for mouse clicks outside the popup
         filter.Popup = popup;
               
      }

      /// <summary>
      /// Responds to the <see cref="System.Windows.Forms.Form.Closed"/>
      /// event from the popup form.
      /// </summary>
      /// <param name="sender">Popup form that has been closed.</param>
      /// <param name="e">Not used.</param>
      private void popup_Closed(object sender, EventArgs e)
      {
         ClosePopup();
      }

      /// <summary>
      /// Subclasses the owning form's existing Window Procedure to enables the 
      /// title bar to remain active when a popup is show, and to detect if
      /// the user clicks onto another application whilst the popup is visible.
      /// </summary>
      /// <param name="m">Window Procedure Message</param>
      protected override void WndProc(ref Message m)
      {
         base.WndProc(ref m);
         if (this.popupShowing)
         {
            // check for WM_ACTIVATE and WM_NCACTIVATE
            if (m.Msg == WM_NCACTIVATE)
            {
               // Check if the title bar will made inactive:
               if (((int) m.WParam) == 0)
               {
                  // If so reactivate it.
                  SendMessage(this.Handle, WM_NCACTIVATE, 1, IntPtr.Zero);
                  
                  // Note it's no good to try and consume this message;
                  // if you try to do that you'll end up with windows
                  // that don't respond.
               }

            }
            else if (m.Msg == WM_ACTIVATEAPP)
            {
               // Check if the application is being deactivated.
               if ((int)m.WParam == 0)
               {
                  // It is so cancel the popup:
                  ClosePopup();
                  // And put the title bar into the inactive state:
                  PostMessage(this.Handle, WM_NCACTIVATE, 0, IntPtr.Zero);
               }
            }
         }
      }

      /// <summary>
      /// Called when the popup is being hidden.
      /// </summary>
      public void ClosePopup()
      {
         if (this.popupShowing)
         {
            if (!skipClose)
            {
               // Raise event to owner
               OnPopupClosed(new PopupClosedEventArgs(this.popup));            
                  
            }
            skipClose = false;

            // Make sure the popup is closed and we've cleaned
            // up:
            this.owner.RemoveOwnedForm(this.popup);
            this.popupShowing = false;
            this.popup.Closed -= popClosedHandler;
            this.popClosedHandler = null;
            this.popup.Close();
            // No longer need to filter for clicks outside the
            // popup.
            Application.RemoveMessageFilter(filter);

            // If we did something from the popup which shifted
            // focus to a new form, like showing another popup
            // or dialog, then Windows won't know how to bring
            // the original owner back to the foreground, so
            // force it here:
            this.owner.Activate();
            
            // Null out references for GC
            this.popup = null;
            this.owner = null;
                        
         }
      }

      /// <summary>
      /// Raises the <see cref="PopupClosed"/> event.
      /// </summary>
      /// <param name="e"><see cref="PopupClosedEventArgs"/> describing the
      /// popup form that is being closed.</param>
      protected virtual void OnPopupClosed(PopupClosedEventArgs e)
      {
         if (this.PopupClosed != null)
         {
            this.PopupClosed(this, e);
         }
      }

      private void popup_Cancel(object sender, PopupCancelEventArgs e)
      {
         OnPopupCancel(e);
      }

      /// <summary>
      /// Raises the <see cref="PopupCancel"/> event.
      /// </summary>
      /// <param name="e"><see cref="PopupCancelEventArgs"/> describing the
      /// popup form that about to be cancelled.</param>
      protected virtual void OnPopupCancel(PopupCancelEventArgs e)
      {
         if (this.PopupCancel != null)
         {
            this.PopupCancel(this, e);
            if (!e.Cancel)
            {
               skipClose = true;
            }
         }
      }

      /// <summary>
      /// Default constructor.
      /// </summary>
      /// <remarks>Use the <see
       cref="System.Windows.Forms.NativeWindow.AssignHandle"/>
      /// method to attach this class to the form you want to show popups
       from.</remarks>
      public PopupWindowHelper()
      {
         filter = new PopupWindowHelperMessageFilter(this);
         filter.PopupCancel += new PopupCancelEventHandler(popup_Cancel);
      }


   }
   #endregion

   #region PopupWindowHelperMessageFilter
   /// <summary>
   /// A Message Loop filter which detect mouse events whilst the popup form is
    shown
   /// and notifies the owning <see cref="PopupWindowHelper"/> class when a
    mouse
   /// click outside the popup occurs.
   /// </summary>
   public class PopupWindowHelperMessageFilter : IMessageFilter
   {
      private const int WM_LBUTTONDOWN = 0x201;
      private const int WM_RBUTTONDOWN = 0x204;
      private const int WM_MBUTTONDOWN = 0x207;
      private const int WM_NCLBUTTONDOWN = 0x0A1;
      private const int WM_NCRBUTTONDOWN = 0x0A4;
      private const int WM_NCMBUTTONDOWN = 0x0A7;

      /// <summary>
      /// Raised when the Popup Window is about to be cancelled.  The
      /// <see cref="PopupCancelEventArgs.Cancel"/> property can be
      /// set to <c>true</c> to prevent the form from being cancelled.
      /// </summary>
      public event PopupCancelEventHandler PopupCancel;
      
      /// <summary>
      /// The popup form
      /// </summary>
      private Form popup = null;
      /// <summary>
      /// The owning <see cref="PopupWindowHelper"/> object.
      /// </summary>
      private PopupWindowHelper owner = null;

      /// <summary>
      /// Constructs a new instance of this class and sets the owning
      /// object.
      /// </summary>
      /// <param name="owner">The <see cref="PopupWindowHelper"/> object
      /// which owns this class.</param>
      public PopupWindowHelperMessageFilter(PopupWindowHelper owner)
      {
         this.owner = owner;
      }

      /// <summary>
      /// Gets/sets the popup form which is being displayed.
      /// </summary>
      public Form Popup
      {
         get
         {
            return this.popup;
         }
         set
         {
            this.popup = value;
         }
      }

      /// <summary>
      /// Checks the message loop for mouse messages whilst the popup
      /// window is displayed.  If one is detected the position is
      /// checked to see if it is outside the form, and the owner
      /// is notified if so.
      /// </summary>
      /// <param name="m">Windows Message about to be processed by the
      /// message loop</param>
      /// <returns><c>true</c> to filter the message, <c>false</c> otherwise.
      /// This implementation always returns <c>false</c>.</returns>
      public bool PreFilterMessage(ref Message m)
      {
         if (this.popup != null)
         {
            switch (m.Msg)
            {            
               case WM_LBUTTONDOWN:
               case WM_RBUTTONDOWN:
               case WM_MBUTTONDOWN:
               case WM_NCLBUTTONDOWN:
               case WM_NCRBUTTONDOWN:
               case WM_NCMBUTTONDOWN:
                  OnMouseDown();
                  break;
            }
         }
         return false;
      }

      /// <summary>
      /// Checks the mouse location and calls the OnCancelPopup method
      /// if the mouse is outside the popup form.      
      /// </summary>
      private void OnMouseDown()
      {
         // Get the cursor location
         Point cursorPos = Cursor.Position;
         // Check if it is within the popup form
         if (!popup.Bounds.Contains(cursorPos))
         {
            // If not, then call to see if it should be closed
            OnCancelPopup(new PopupCancelEventArgs(popup, cursorPos));
         }
      }

      /// <summary>
      /// Raises the <see cref="PopupCancel"/> event.
      /// </summary>
      /// <param name="e">The <see cref="PopupCancelEventArgs"/> associated 
      /// with the cancel event.</param>
      protected virtual void OnCancelPopup(PopupCancelEventArgs e)
      {
         if (this.PopupCancel != null)
         {
            this.PopupCancel(this, e);
         }
         if (!e.Cancel)
         {
            owner.ClosePopup();
            // Clear reference for GC
            popup = null;
         }
      }


   }
   #endregion

}