vbAccelerator - Contents of code file: DragDropTextBox.cs

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

using vbAccelerator.Components.ImageList;

namespace vbAccelerator.Controls.TextBox
{
   /// <summary>
   /// An enhanced text box which supports drag and drop
   /// of the text contents.
   /// </summary>
   public class DragDropTextBox : System.Windows.Forms.TextBox
   {

      /// <summary>
      /// Maintains the start position of the last selection
      /// made with the mouse.
      /// </summary>
      protected int lastSelStart = -1;
      /// <summary>
      /// Maintains the length of the last selection made with 
      /// the mouse.
      /// </summary>
      protected int lastSelLength = -1;
      /// <summary>
      /// The ImageListDrag class which is used when the 
      /// DrawDragImage property is set.
      /// </summary>
      protected ImageListDrag imageListDrag = new ImageListDrag();
      /// <summary>
      /// Whether the drag image is drawn or not.
      /// </summary>
      protected bool drawDragImage = false;
      /// <summary>
      /// Whether a mouse down event is a candidate for a drag event.
      /// </summary>
      protected bool dragCandidateAction = false;
      /// <summary>
      /// The point at which the mouse down for a dragCandidateAction occurred.
      /// </summary>
      protected Point dragDownPoint;
      /// <summary>
      /// Flag set when initially showing the custom drag image so we can
      /// ensure the TextBox isn't corrupted.
      /// </summary>
      private bool refreshControl = false;

      /// <summary>
      /// Gets/sets whether a drag image will be created and
      /// drawn during dragging or not.  Defaults to <code>False</code>.
      /// </summary>
      public bool DrawDragImage
      {
         get
         {
            return this.drawDragImage;
         }
         set
         {
            this.drawDragImage = value;
         }
      }

      private bool DoDrawDragImage
      {
         get
         {
            return ((this.drawDragImage) && (this.ImageList != null));
         }
      }

      /// <summary>
      /// Gets/sets the ImageList to use to construct the custom
      /// drag image.
      /// </summary>
      public System.Windows.Forms.ImageList ImageList
      {
         get
         {
            return this.imageListDrag.Imagelist;
         }
         set
         {
            this.imageListDrag.Imagelist = value;
         }
      }


      private void constructDragImage()
      {
         System.Windows.Forms.ImageList ils = this.ImageList;
         
         // Clear images in image list:
         ils.Images.Clear();
         // ImageList is buggy, need to ensure we do this:
         IntPtr ilsHandle = ils.Handle;

         // Create the bitmap to hold the drag image:
         Bitmap bitmap = new Bitmap(ils.ImageSize.Width, ils.ImageSize.Height);
         
         // Get a graphics object from it:
         Graphics gfx = Graphics.FromImage(bitmap);

         // Default fill the bitmap with black:
         gfx.FillRectangle(Brushes.Black, 0, 0, bitmap.Width, bitmap.Height);

         // Draw text in highlighted form:
         StringFormat fmt = new StringFormat(StringFormatFlags.LineLimit);
         fmt.Alignment = StringAlignment.Center;            
         SizeF size = gfx.MeasureString(this.SelectedText, this.Font,
          bitmap.Width, fmt);
         float left = 0F;
         if (size.Height> bitmap.Height)
         {
            size.Height = bitmap.Height;
         }
         if (size.Width < bitmap.Width)
         {
            left = (bitmap.Width - size.Width)/2F;
         }
         RectangleF textRect = new RectangleF(
            left, 0F, size.Width, size.Height);
         gfx.FillRectangle(SystemBrushes.Highlight, textRect);
         gfx.DrawString(this.SelectedText, this.Font,
          SystemBrushes.HighlightText,
            textRect, fmt);
         fmt.Dispose();

         // Add the image to the ImageList:
         ils.Images.Add(bitmap, Color.Black);

         // Clear up the graphics object:
         gfx.Dispose();
         // Clear up the bitmap:
         bitmap.Dispose();

         Trace.Assert(ils.Images.Count > 0, "No images in drag image list!!!");
      }

   
      /// <summary>
      /// Raises the MouseDown event and prepares to start a drag operation
      /// if appropriate.
      /// </summary>
      /// <param name="e">A <code>MouseEventArgs</code> object
      /// with the details of the mouse event.</param>
      protected override void OnMouseDown(MouseEventArgs e)
      {
         if (e.Button == MouseButtons.Left && 
            (this.SelectionStart >= this.lastSelStart) && 
            (this.SelectionStart <= this.lastSelStart + this.lastSelLength) &&
            this.lastSelLength > 0)
         {
            base.OnMouseDown(e);

            this.SelectionStart = this.lastSelStart;
            this.SelectionLength = this.lastSelLength;
            this.Update();
            this.dragDownPoint = new Point(e.X, e.Y);
            this.dragCandidateAction = true;
         }
         else
         {
            base.OnMouseDown(e);

            this.lastSelStart = this.SelectionStart;
            this.lastSelLength = this.SelectionLength;            
            this.dragCandidateAction = false;
         }
      }

      /// <summary>
      /// Raises the MouseMove event and starts a drag 
      /// operation if appropriate.
      /// </summary>
      /// <param name="e">A <code>MouseEventArgs</code> object 
      /// with the details of the mouse event.</param>
      protected override void OnMouseMove(MouseEventArgs e)
      {
         base.OnMouseMove(e);

         if (e.Button == MouseButtons.Left && 
            this.dragCandidateAction && 
            ((Math.Abs(e.X - this.dragDownPoint.X)  > 4) ||
            (Math.Abs(e.Y - this.dragDownPoint.Y) > 4)))
         {
            this.dragCandidateAction = false;
            this.SelectionStart = this.lastSelStart;
            this.SelectionLength = this.lastSelLength;
               
            if (DoDrawDragImage)
            {
               constructDragImage();
               Trace.Assert(this.ImageList.Images.Count > 0, "No images in drag
                image list!!!");
               this.imageListDrag.StartDrag(0, 
                  this.imageListDrag.Imagelist.ImageSize.Width / 2, 
                  -this.imageListDrag.Imagelist.ImageSize.Height);             
                    
               this.refreshControl = true;
            }
               
            this.DoDragDrop(this.SelectedText, DragDropEffects.Move |
             DragDropEffects.Copy);
               
            if (DoDrawDragImage)
            {
               this.imageListDrag.CompleteDrag();
            }
         }
      }
      

      /// <summary>
      /// Raises the MouseUp event and stores some information
      /// about the mouse up position to track whether the next
      /// mouse operation should initiate a drag.
      /// </summary>
      /// <param name="e">A <code>MouseEventArgs</code> object
      /// with the details of the mouse event.</param>
      protected override void OnMouseUp(MouseEventArgs e)
      {
         base.OnMouseUp(e);

         if (this.dragCandidateAction)
         {
            this.SelectionLength = 0;
            this.lastSelLength = 0;
         }
         else
         {
            this.lastSelStart = this.SelectionStart;
            this.lastSelLength = this.SelectionLength;
         }
         this.dragCandidateAction = false;         
      }

      /// <summary>
      /// Raises the <code>DragOver</code> event, and if the client
      /// sets the <code>Effect</code> member to allow
      /// the data to be dropped, attempts to show
      /// the caret in the position at which the item
      /// would The standard System.Windows.Forms drag-drop functionality
       provides a cursor to indicate that a drag-drop function is in progress.
       This article demonstrates how to add an image of the object being
       dragged to the drag-drop control, in the same way that Explorer does,
       using the ImageList APIs.be dropped.
      /// </summary>
      /// <param name="e">A <code>DragEventArgs</code> object containing 
      /// the data associated with the <code>DragOver</code> event.</param>
      protected override void OnDragOver(DragEventArgs e)
      {
         base.OnDragOver(e);

         if (e.Effect != DragDropEffects.None)
         {

            // need to position the caret according to the drag
            // position (this used to be automatic in VB)
            Point pt = this.PointToClient(new Point(e.X, e.Y));
            int index = TextBoxDragDropHelper.CharFromPos(this, pt);

            this.SelectionStart = index;
            this.SelectionLength = 0;
            TextBoxDragDropHelper.ShowCaret(this);
         }

      }

      /// <summary>
      /// Raises the GiveFeedback event and displays the 
      /// customised drag image, if any.
      /// </summary>
      /// <param name="e">The details associated with the
      /// GiveFeedback event.</param>
      protected override void OnGiveFeedback(GiveFeedbackEventArgs e)
      {
         // Raise the GiveFeedback event
         base.OnGiveFeedback(e);

         // Draw the drag image:
         if (DoDrawDragImage)
         {
            if (this.refreshControl)
            {
               imageListDrag.HideDragImage(true);
               this.Update();
               imageListDrag.HideDragImage(false);
               this.refreshControl = false;
            }
            imageListDrag.DragDrop();
         }
      }

      /// <summary>
      /// Raises the <code>DragDrop</code> event and if the client
      /// sets the <code>Effect</code> member to allow
      /// the data to be dropped, calls the <code>GetTextDropData</code>
      /// method to retrieve the text from the drag-drop data and inserts
      /// it into the control at the drag-drop point.
      /// </summary>
      /// <param name="e">A <code>DragEventArgs</code> object containing 
      /// the data associated with the <code>DragDrop</code> event.</param>
      protected override void OnDragDrop(DragEventArgs e)
      {      
         base.OnDragDrop(e);

         this.Focus();

         if (e.Effect != DragDropEffects.None)
         {
            string data = GetTextDropData(e);
            if (e.Effect == DragDropEffects.Move)
            {
               int dropPos = this.SelectionStart;

               if (this.lastSelStart > dropPos)
               {
                  // if the original text position is after the point
                  // we're dropping to, then we can just delete the 
                  // original text and drop again:
                  this.SelectionStart = this.lastSelStart;
                  this.SelectionLength = this.lastSelLength;
                  this.SelectedText = "";
                  this.SelectionStart = dropPos;
                  this.SelectionLength = 0;
                  this.SelectedText = data;
               }
               else
               {
                  // Once we've removed the text at the original
                  // position, the drop pos is going to move up
                  // by the number of characters in the original 
                  // selection:                  
                  this.SelectionStart = this.lastSelStart;
                  this.SelectionLength = this.lastSelLength;
                  dropPos -= this.lastSelLength;
                  this.SelectedText = "";
                  this.SelectionStart = dropPos;
                  this.SelectionLength = 0;
                  this.SelectedText = data;
               }
            }
            else
            {
               this.SelectedText = data;
            }

            this.Invalidate();
         }
         
         this.lastSelStart = -1;
         this.lastSelLength = -1;
      }

      /// <summary>
      /// Gets the text data for the drag event args. The preferred
      /// format to use is <code>UnicodeText</code>, followed by
      /// <code>StringFormat</code>.  If neither of these are present
      /// then the return from <code>ToString</code> on the first format 
      /// returned by the <code>GetFormats</code> method of the 
      /// <code>DragEventArgs</code> <code>Data</code> object is used.
      /// </summary>
      /// <param name="e">The event details associated with the 
      /// DragDrop event.</param>
      /// <returns>A string containing the text to drop.</returns>
      protected virtual string GetTextDropData(DragEventArgs e)
      {
         string[] formats = e.Data.GetFormats();
         string useFormat = "";
         bool firstTime = true;
         foreach (string format in formats)
         {
            if (firstTime)
            {
               useFormat = format;
               firstTime = false;
            }
            else
            {
               if (format.Equals(DataFormats.UnicodeText))
               {
                  useFormat = format;
                  break;
               }
               else if (format.Equals(DataFormats.StringFormat))
               {
                  useFormat = format;
               }
            }
         }
         object data = e.Data.GetData(useFormat);
         
         return data.ToString();
      }
   }


   /// <summary>
   /// Wraps API calls for access to missing functionality
   /// from the System.Windows.Forms text box.
   /// </summary>
   public class TextBoxDragDropHelper
   {
      #region Unmanaged Code
      [DllImport("user32", CharSet=CharSet.Auto, EntryPoint="SendMessage")]
      private extern static int SendMessageInt(
         IntPtr handle,
         int msg,
         int wParam,
         int lParam
         );
      private const int EM_LINEINDEX           = 0x00BB;
      private const int EM_POSFROMCHAR          = 0x00D6;
      private const int EM_CHARFROMPOS          = 0x00D7;

      [DllImport("user32", EntryPoint="ShowCaret")]
      private extern static bool ShowCaretAPI (
         IntPtr hwnd );
      #endregion

      /// <summary>
      /// Attempts to make the caret visible in a TextBox control.
      /// This will not always succeed since the TextBox control
      /// appears to destroy its caret fairly frequently.
      /// </summary>
      /// <param name="txt">The text box to show the caret in.</param>
      public static void ShowCaret(
         System.Windows.Forms.TextBox txt
         )
      {
         bool ret = false;
         int iter = 0;
         while (!ret && iter < 10)
         {
            ret = ShowCaretAPI(txt.Handle);
            iter++;
         }
      }

      /// <summary>
      /// Returns the index of the character under the specified 
      /// point in the control, or the nearest character if there
      /// is no character under the point.
      /// </summary>
      /// <param name="txt">The text box control to check.</param>
      /// <param name="pt">The point to find the character for, 
      /// specified relative to the client area of the text box.</param>
      /// <returns></returns>
      public static int CharFromPos(
         System.Windows.Forms.TextBox txt,
         Point pt
         )
      {
         unchecked
         {
            // Convert the point into a DWord with horizontal position
            // in the loword and vertical position in the hiword:
            int xy = (pt.X & 0xFFFF) + ((pt.Y & 0xFFFF) << 16);
            // Get the position from the text box.
            int res = SendMessageInt(txt.Handle, EM_CHARFROMPOS, 0, xy);
            // the Platform SDK appears to be incorrect on this matter.
            // the hiword is the line number and the loword is the index
            // of the character on this line
            int lineNumber = ((res & 0xFFFF) >> 16);
            int charIndex = (res & 0xFFFF);
            
            // Find the index of the first character on the line within 
            // the control:
            int lineStartIndex = SendMessageInt(txt.Handle, EM_LINEINDEX,
             lineNumber, 0);
            // Return the combined index:
            return lineStartIndex + charIndex;
         }
      }

      /// <summary>
      /// Returns the position of the specified character
      /// </summary>
      /// <param name="txt">The text box to find the character in.</param>
      /// <param name="charIndex">The index of the character whose
      /// position needs to be found.</param>
      /// <returns>The position of the character relative to the client
      /// area of the control.</returns>
      public static Point PosFromChar(
         System.Windows.Forms.TextBox txt,
         int charIndex
         )
      {
         unchecked
         {
            int xy = SendMessageInt(txt.Handle, EM_POSFROMCHAR, charIndex, 0);
            return new Point(xy);
         }
      }

      // private constructor, methods are static
      private TextBoxDragDropHelper()
      {
         // intentionally left blank
      }
   }


}