Custom Windows Forms Controls: ColorPicker.NET

 

March 2005

Chris Sano
Microsoft Developer Network

Summary: Chris Sano introduces you to ColorPicker.NET and demonstrates techniques that were used to build some of the custom controls in the application. (27 printed pages)

Applies to:
   Microsoft .NET Framework 1.1
   Visual Basic .NET
   Visual C#

Code Sample

There are two different sets of code samples that accompany this article:
    1. A recent snapshot of the ColorPicker.NET source tree (C# only).
    2. The code discussed in this article (C# and Visual Basic .NET).

Download CustomWindowsFormsControls_ColorPicker.msi.

Contents

Introduction
How the Application Works
The Color Slider
   Using the Color Slider in Your Application
   Color Slider Infrastructure
   The Color Slider in ColorPicker.NET
Drag and Drop
   Drag-and-Drop Infrastructure
   Drag and Drop in ColorPicker.NET
Conclusion
About the Author

Introduction

ColorPicker.NET, as seen in Figure 1, is the result of a small personal project that I undertook in an attempt to reverse engineer the functionality in the Adobe Photoshop color picker. I do a lot of user interface design work, and in place of firing up Photoshop every time I needed to experiment with some colors, I wanted a lightweight application that would allow me to adjust and sample colors from the HSB and RGB color spaces, and provide the ability to save the colors that I liked best to a swatch panel for future use.

Figure 1. Main ColorPicker.NET interface

I wrote this article to provide insight into the implementation of some of the more interesting parts of the application. First I will discuss the color slider, and then I will describe the drag-and-drop experience involved in dragging the currently selected color to the color swatch panel. Future installments will cover other areas of the application.

How the Application Works

Before we get started, I'm going to provide a brief overlook of the functionality available in ColorPicker.NET, as well as the different terminology used throughout the article.

ColorPicker.NET has two different color spaces, which are theoretical three-dimensional color systems in which coordinates are used to represent colors. The red-green-blue (RGB) color space is created by mapping those colors onto a three-dimensional Cartesian coordinate system. The hue-saturation-brightness (HSB) color space works differently, as it is mapped onto a cylindrical coordinate system. Hue represents the actual color, saturation expresses the strength or purity of the color, and brightness is the relative brightness or darkness of the color.

The color slider represents the range of values of the selected color space component. The other two components are represented on the axes in the color field, which is the area on the far left of the interface in Figure 1. For instance, if you select the green color space component in the RGB color space, the range of values of the green coordinate would be represented by the color slider, and the red and blue coordinates would be represented by the x and y axes respectively, in the color field. As another example, by selecting the hue color space component, the range of the hue values would be represented by the color slider and the saturation and brightness values would be represented by the x and y axes respectively, in the color field.

When you click on the color field, a circular marker appears. The very center point of this marker represents the color that is selected. This color appears in the currently selected color window above the HSB color space. If you change the value of the selected color space component using the color slider, the currently selected color window will reflect the color that resides at the location of the circular marker.

You can change the values of the individual color space components by keying in the desired values in the component textbox. You can also type or paste HEX values in the HEX textbox.

The color swatch panel area on the right of the interface in Figure 1 contains predefined color swatches. You can retrieve the color space or hex values of the individual swatches by clicking on them. If you want to save any of the colors that you have selected, you can add to the existing swatches by clicking on the currently selected color window and dragging the color to the color swatch panel.

The Color Slider

The color slider control in ColorPicker.NET, much like the color slider in the Photoshop color picker dialog, provides a means of adjusting the value of the currently selected color space component using the mouse. Users are able to select a value between 0 and 255 by dragging the arrows or clicking anywhere within the defined color region. Even though the control displays a color gradient, it does not perform any color-related calculations.

This section is broken down into three distinct components. First, I'll explain how easy it is to add the color slider control to a form, and prepare it for use. This will give you an idea of what is involved in making it work. Second, I'll examine the infrastructure of the control and explain how the control was built. Finally, I'll explain how I incorporated the color slider into the color picker application and discuss some of the problems that I encountered while doing so.

Using the Color Slider in Your Application

Figure 2. A form containing the Color Slider control

To add the color slider control (shown in Figure 2) to your form or another control, you need to define and initialize the ColorSlider object and set a few of its layout properties, shown in the following code block. The instance also subscribes to the ValueChanged event that the control publishes to allow it to listen for changes in the control's value.

public class ColorSliderForm : Form {

  ColorSlider colorSlider1;

  public ColorSliderForm() {

    this.colorSlider1 = new ColorSlider();
    this.colorSlider1.Location = new System.Drawing.Point( 8, 16 );
    this.colorSlider1.Size = new System.Drawing.Size( 64, 328 );
    this.colorSlider1.ValueChanged += new ValueChangedHandler( this.colorSlider1_ValueChanged );

  
  }

}

The color slider exposes a read-only property named ColorBitmap that provides a reference to an 18- by 256-pixel bitmap object. You can use this to define the color that shows up inside the middle rectangle of the color slider (henceforth referred to as the color region), as seen in Figure 2. There are no restrictions placed on the colors that you can use, but it makes the most sense to designate colors that conform to the values that the color slider represents. Below, I create a linear gradient that spans from black to red at 270 degrees from the gradient's orientation line.

public class ColorSliderForm : Form {

  public ColorSliderForm() {

    ...
          
    using ( Graphics g = Graphics.FromImage( colorSlider1.ColorBitmap ) ) {

      Color start = Color.FromArgb( 0, 0, 0 );
      Color end = Color.FromArgb( 255, 0, 0 );
         
      Rectangle region = new Rectangle( 0, 0, 18, 300 );
      using ( LinearGradientBrush lgb = new LinearGradientBrush( region, start, end, 270f ) ) {

        g.FillRectangle( lgb, region );

      }
    
    }
  
  }

}

In the example seen in Figure 2, the text box is updated with the new value of the color slider through the ValueChanged event handler. By grabbing and moving the arrows, or clicking anywhere on the color region, you can see this value change.

Color Slider Infrastructure

The color slider places a large burden on the accuracy of the painting operations in its infrastructure. Even though there is not much to the control, there is a lot of painting involved in response to user actions and a smooth, flicker-free experience is very important. In this section, I will be walking through the implementation of the control, focusing on techniques that will ensure that the transitions between the values in the slider are as fluid as possible.

Figure 3. Components of the color slider

Before taking a look at the code, I want to point out the drawn components of the color slider control that are manifested in Figure 3. The outer region is a solid line whose purpose is purely aesthetic. It is represented in the code through the m_outerRegion field. The color region, as discussed earlier, is used to display the color and is represented through the m_colorRegion field. The left and right arrow regions are represented by the m_leftArrowRegion and m_rightArrowRegion fields respectively. The vertical position of the arrows is stored in m_currentArrowYLocation. The said identifiers are seen throughout the various code blocks that exist throughout the remainder of this section.

When the color slider is initialized, everything described in the previous paragraph is defined. The region identifiers and the color bitmap are defined as read-only values. The vertical location of the arrows is updated to the location of the top portion of the color region in the constructor.

public class ColorSlider : UserControl {

  // private data member declarations
  private int m_currentArrowYLocation;
  private Rectangle m_leftArrowRegion;
  private Rectangle m_rightArrowRegion;

  // readonly data member declarations
  private readonly Rectangle m_colorRegion = new Rectangle( 13, 7, 18, 256 );
  private readonly Rectangle m_outerRegion = new Rectangle( 10, 4, 26, 264 );
  private readonly Bitmap m_colorBitmap = new Bitmap( 18, 256 );
 
  public ColorSlider() : base() {
    m_currentArrowYLocation = m_colorRegion.Top;
  }

}

The OnPaint method is overridden to draw the boundaries and the arrows. The arrows are drawn with the assistance of the CreateLeftTrianglePointer and CreateRightTrianglePointer helper methods, which are shown in the code block after this one.

public class ColorSlider : UserControl {
  
  protected override void OnPaint(PaintEventArgs e) {

    base.OnPaint (e);

    using ( Graphics g = e.Graphics ) {

      CreateLeftTrianglePointer( g, m_currentArrowYLocation );
      CreateRightTrianglePointer( g, m_currentArrowYLocation ); 

      if ( m_colorBitmap != null ) {
          g.DrawImage( m_colorBitmap, m_colorRegion );
      }

      ControlPaint.DrawBorder3D( g, m_outerRegion );
      g.DrawRectangle( Pens.Black, m_colorRegion );

      }

    }

  }

}

The following helper methods create an array of points that are used to determine where the arrows are to be drawn. The topmost portion of the triangle is calculated first, followed by the pointer and then the bottom. The triangle is then drawn by passing the points into the DrawPolygon method of the Graphics object.

public class ColorSlider : UserControl {

  private void CreateLeftTrianglePointer( Graphics g, int y ) {

    Point[] points = { 
      new Point( m_outerRegion.Left - ARROW_WIDTH - 1, y - ( ARROW_HEIGHT / 2 ) ), // top 
      new Point( m_outerRegion.Left - 2, y ), // pointer
      new Point( m_outerRegion.Left - ARROW_WIDTH - 1, y + ( ARROW_HEIGHT / 2 ) ) // bottom
    };
   
    g.DrawPolygon( Pens.Black, points );

  }

  private void CreateRightTrianglePointer( Graphics g, int y ) {

    Point[] points = { 
      new Point( m_outerRegion.Right - 1 + ARROW_WIDTH, y - ( ARROW_HEIGHT / 2 ) ), // top
      new Point( m_outerRegion.Right, y ), // pointer
      new Point( m_outerRegion.Right - 1 + ARROW_WIDTH, y + ( ARROW_HEIGHT / 2 ) ) // bottom
    };
   
    g.DrawPolygon( Pens.Black, points );
   
  }

}

The fun starts when the user presses the mouse button. Before raising the MouseDown event, the control checks to see if the left mouse button was pressed. If it was, the m_isLeftMouseButtonDown flag is set and the CheckCursorYRegion method is invoked with the y-coordinate value.

public class ColorSlider : UserControl {

  protected override void OnMouseDown(MouseEventArgs e) {
         
    if ( e.Button == MouseButtons.Left ) {
               
      m_isLeftMouseButtonDown = true;
      CheckCursorYRegion( e.Y );      
      
    }

    base.OnMouseDown (e);
      
  }

}

If the user moves the mouse cursor while the left mouse button is down, the m_isLeftMouseButtonDownAndMoving flag is set to true and the CheckCursorYRegion method is invoked.

public class ColorSlider : UserControl {

  protected override void OnMouseMove(MouseEventArgs e) {

    if ( m_isLeftMouseButtonDown ) {   

      m_isLeftMouseButtonDownAndMoving = true;
      CheckCursorYRegion( e.Y );         
   
    }
   
    base.OnMouseMove (e);

  }

}

There are two conditions that must be met in CheckCursorYRegion. First, if the user has pressed the left mouse button, the y-coordinate must lie in between the top and bottom of the color region. If this condition is not met, the method returns and nothing happens.

Figure 4. Issues with cursor positioning

When the user moves the mouse cursor from point A to point B, there is no guarantee that Windows will provide all of the coordinates in between. In fact, it rarely ever does. In some cases, you might have something like what is displayed in the first image in the figure above where you have explicitly instructed that the y-coordinate of the cursor is to be in between the top and bottom area of the color region in order for the arrow positions to be updated. The user has dragged the mouse cursor so quickly that the last coordinate that Windows has provided that falls within the restricted zone is where the arrows lie. The ideal behavior here would be the arrows being positioned at the top portion of the color region, as displayed in the second image. In order to make this happen, a conditional expression was added that checks to see if the mouse button is down and is being moved (m_isLeftMouseButtonDownAndMoving). If it is, and the y-coordinate is outside the color region, then the value is adjusted according to the cursor position. If the cursor is above the top of the color region, then the arrows are positioned at the top. If it's below the bottom of the color region, the arrows are positioned at the bottom.

public class ColorSliderForm : Form {
  
  private void CheckCursorYRegion( int y ) {

    int mValue = y;

    if ( m_isLeftMouseButtonDown && !m_isLeftMouseButtonDownAndMoving ) {

      if ( y < m_colorRegion.Top || y > m_colorRegion.Bottom ) {
        return;
      }

    } else if ( m_isLeftMouseButtonDownAndMoving ) {

      if ( y < m_colorRegion.Top ) {
        mValue = m_colorRegion.Top;
      } else if ( y >= m_colorRegion.Bottom ) {
        mValue = m_colorRegion.Bottom - 1;
      }

    } else {
      return;
    }

    ...

  }

}

After all the required conditions have been met, the vertical location of the arrows (m_currentArrowYLocation) is updated with the modified y-coordinate value. The arrow regions are then invalidated through a call to the InvalidateArrowRegions method.

public class ColorSliderForm : Form {

  private void CheckCursorYRegion( int y ) {

    ...

    m_currentArrowYLocation = mValue;
    InvalidateArrowRegions( mValue );

    ...

  }

}

When the InvalidateArrowRegions method is invoked, the regions stored in m_leftArrowRegion and m_rightArrowRegion are invalidated. After that, the new regions are created through calls to the GetLeftTrianglePointerInvalidationRegion and GetRightTrianglePointerInvalidationRegion methods, which take in the new y-value and calculate the new regions. The new regions are then invalidated to let the system know that painting instructions await its attention.

public class ColorSliderForm : Form {

  private void InvalidateArrowRegions( int y ) {

    this.Invalidate( m_leftArrowRegion ); // left of marker a in figure 5
    this.Invalidate( m_rightArrowRegion ); // right of marker a in figure 5
      
    m_leftArrowRegion = this.GetLeftTrianglePointerInvalidationRegion( y );
    m_rightArrowRegion = this.GetRightTrianglePointerInvalidationRegion( y );

    this.Invalidate( m_leftArrowRegion ); // left of marker b in figure 5
    this.Invalidate( m_rightArrowRegion ); // right of marker b in figure 5

  }

}

Figure 5. (1) Invalidated arrow regions; (2) after painting has taken place.

I've provided Figure 5 to give a visual representation of the invalidation process. Marker a in the first (left) image represents the arrows at their initial location. Imagine that the user has clicked on the color region in the area where marker b resides. The translucent red rectangles that are drawn on top of the arrows and across from marker b represent the areas that are added to the update region of the control. When the control receives its next paint message, it will trigger the necessary paint operations on the regions listed in the update region, giving us the view seen in the second image.

The last thing that needs to happen is the generation of the ValueChanged event to notify all listeners that the color slider has a new value. The event data, represented by ValueChangedEventArgs, has a parameterized constructor that requires this new value. Instead of the minimum value of the slider being on top and the maximum value being on the bottom, it's the other way around, so we need to do a quick calculation to determine the value before the event can be raised.

Recall that mouse coordinates are expressed in relation to the top left corner of the window. The more you move down, the higher the y-coordinate is. In the color slider, the more you move up, the higher the value. To work around this anomaly, we subtract the difference between the y-coordinate and the value of the topmost portion of the color region from 255. For instance, if the y-coordinate was 250 and the top of the color region was 150, the value would be 155 (255 – (250 – 150) = 155).

public class ColorSliderForm : Form {

  private void CheckCursorYRegion( int y ) {

    ...

    if ( ValueChanged != null ) {
      ValueChanged( this, new ValueChangedEventArgs( 255 - ( mValue – m_colorRegion.Top ) ) );
    }   

  }

}

The Color Slider in ColorPicker.NET

The value returned by the color slider works well for the coordinates in the RGB color space, since their values can range from 0-255. However, some adjustments are required in order to make it work with the coordinates in the HSB color space. The hue coordinate is measured in degrees between 0 and 360 inclusive and the brightness and saturation coordinates are measured in percentages. When the color slider value is received through the ValueChanged event handler, calculations are performed to obtain the appropriate hue, saturation, and brightness values.

In the application, the color slider is tightly integrated into the color panel control, which is the main control in ColorPicker.NET at this time. This was done because there are so many different dependencies, and it was too difficult to try to get the color slider to work as an independent control. One of the problems that I ran into during the initial development stages was dealing with lag time. When you change the color slider value, there are, at a minimum, four different controls that need to be updated: the color field, the currently selected color window, the active coordinate, and the opposing color space (that is, if an RGB value is changed, the HSB color space needs to be updated). When I attempted to slide from point A to point B, even though in many cases the change was minimal, the color transition in both the color field and the picture box was very choppy. This produced a very unpleasant user experience and was something that I needed to correct.

I wrote a simple test application that tested the difference between coordinate values supplied by sequential WM_MOUSEMOVE messages. The largest discrepancy that I could achieve between two messages in a window of the same size as the main ColorPicker window was 308. Granted, this is outside the range of values provided by the color slider, but it does show that it is entirely possible for the user to go from 0-255 within one message cycle. Imagine the visual disruption of an RGB value going from RGB: 0, 0, 0 to RGB: 255, 0, 0 without any fluidity.

I was able to fix this problem with the enlistment of a System.Windows.Forms.Timer object and several different private data fields. The objective with the timer was to gradually update the color panels and spaces to smooth out the irregularities that were being seen. It progressively reduces the gap between the old and target values by updating the current value with each tick.

Before the timer is started, the three private data fields need to be initialized. The following bulleted list manifests the purpose of each field.

  • m_targetYValue represents the coordinate value supplied by the most recent WM_MOUSEMOVE message.
  • m_currentYValue is used to track the value as it is moved from the old value to the target value. This is set to the value that was conveyed through the previous WM_MOUSEMOVE message (which is stored in m_oldValue).
  • m_oldValue is updated with the same value as m_targetYValue. This value is checked in a conditional statement to make sure that the timer does not start if the ValueChanged method is invoked with the same value as it was the previous time.

The timer is then started with a tick value of 1, which means that the Tick event gets fired once every millisecond.

public class ColorPanel : UserControl {

  private void ValueChanged( int newValue ) {

    ...

    if ( m_oldValue != mValue && m_panelState[ PANELSTATE_isLeftMouseDownAndMoving ] ) {

      m_targetYValue = newValue;      m_currentYValue = m_oldValue;      m_oldValue = newValue;       sliderTimer.Start();            

    }

    ...

  }

}

With every tick, the difference between the current and target values is checked. It is then split in half to determine the incremental amount of the current value. This process is repeated until the current value is equal to the target value.

public class ColorPanel : UserControl {

  private void sliderTimer_Tick(object sender, System.EventArgs e) {
      
    int difference = m_currentYValue - m_targetYValue;         
    int nextValue = ( int ) Math.Round( ( double ) difference / 2 );

    if ( nextValue == 0  ) {

      m_currentYValue = m_targetYValue;
      sliderTimer.Stop();

    } else  {

      if ( m_currentYValue < m_targetYValue ) {
          m_currentYValue += nextValue;
      } else {
          m_currentYValue += -nextValue;
      }

    } 
      
    ...

  }

}

For example, if the current value is 120 and the target value is 220, the first time the tick method is invoked, the difference would be 100 and the incremental value would be 50. The second time, it would be 50 and 25. The third time, it would be 25 and 12, and so forth.

With this solution in place, instead of the color slider jumping from, say, 110 to 210, it will go from 110 to 160 to 185 to 197 to 203 to 207 to 209 and finally to 210. The color panels and spaces are updated at each tick, creating the smooth transitional effect that I wanted.

Before concluding the discussion on this control, I want to note that FXCop complains about the tick frequency, warning that higher frequency periodic activity will keep the CPU busy and interfere with the tasks that it might need to perform. Since the timer will run for no longer than 10 milliseconds at a time, there should be no cause for concern. You will see a temporary spike in CPU usage as you are changing the value, but that drops back down to normal levels fairly quickly.

Drag and Drop

The drag-and-drop functionality in ColorPicker.NET allows users to build up their own custom swatch palette by dragging colors from the currently selected color window to the color swatch panel. This experience is based on what the .NET Framework provides through the Win32 OLE drag-and-drop model, but is extended to provide a more aesthetic dragging experience for the user.

Figure 6. The drag-and-drop experience

This section is broken down into two parts. As with the color slider, I'll go through the implementation details of the drag-and-drop experience and then I will briefly detail how I implemented this functionality in ColorPicker.NET.

Drag-and-Drop Infrastructure

The OLE model does not provide anything that allows you to visually enhance the drag-and-drop experience, and unfortunately, nothing was added in this area when the .NET wrapper was written. This meant that if I were to add the transparent rectangle that I had so badly wanted to, I would be forced to do all the dirty work myself.

After researching my options, I decided that in lieu of implementing global system hooks to track the cursor position and perform the necessary nonclient painting, I would emulate the painting by using a transparent form. The forthcoming paragraphs will provide you with the details of the various functionalities that I pulled together to make this happen.

In order to make the whole thing work, the form needed to meet the following criteria: (1) it was not to show up in the task bar; (2) it needed to be the top-most window; (3) its icon could not show up in the ALT+TAB window if the user pressed ALT+TAB during a drag-and-drop operation; (4) and, most importantly, it was not to steal focus from the application when displayed.

The DragForm constructor defines the properties needed to meet the first three criteria. Note that the only way to prevent the icon of a form from showing up in the ALT+TAB window is to make it a tool window (either fixed or sized).

internal class DragForm : Form {
  
  internal DragForm() : base() {

    this.ShowInTaskbar = false;
    this.TopMost = true;
    this.FormBorderStyle = FormBorderStyle.FixedToolWindow;

  }

}

In order to meet the fourth criterion, a brief foray into unmanaged code was required. The ShowWindow function in the User32 library enables you to explicitly instruct Windows not to activate the window when it's shown. By using this function, it is guaranteed that the form that was active at the time that the DragForm instance is shown will remain active even though the instance is visible and resides higher in the z-order.

Since the Show method in the base class is nonvirtual, a new method named ShowForm is created. The call to ShowWindow is wrapped in this method. The only downside is that those who use this class will have to remember to invoke ShowForm instead of Show if they want to ensure that the underlying form remains active.

internal class DragForm : Form {
  
  [DllImport( "User32.dll" )]
  private static extern IntPtr ShowWindow( IntPtr hwnd, long nCmdShow );

  internal void ShowForm() {
    ShowWindow( this.Handle, SW_SHOWNOACTIVATE );
  }

}

Now we've satisfied all four requirements in the criteria list. We're not done, though. I know we haven't discussed the drag-and-drop functionality yet, but I'm going to jump ahead a little bit. The following figure contains a screenshot of the source control after the mouse button has been pressed. The drag-and-drop operation has started, and the DragForm instance is shown.

Figure 7. Borders of the tool window are visible

There's one little problem: the tile bar and the borders of the tool window are visible. It would have been easy to just change the form border style to none, but recall that in order to prevent the form's icon from showing up in the ALT+TAB window (criterion #3), the border style of the form must be set to a tool window style. Instead, I decided to add a method that would expand the client area and modify the region of the form so that the title bar and borders were excluded.

The ChangeSize method displayed below has a Size parameter, which is expected to be a representation of the desired client area. The parameter value is stored in a private data field named m_clientRectangleSize for use in the OnPaint method that I will be discussing momentarily. The Size property of the form is updated with the sum of the Size parameter and another Size object that represents the total amount of horizontal and vertical space that is consumed by the title bar and borders.

internal class DragForm : Form {
  
  internal void ChangeSize( Size newSize ) {

    m_clientRectangleSize = newSize;
    this.Size = newSize + new Size( 6, 22 );

    ...

  }

}

This gives us the view in the first image in Figure 8. You might not be able to tell, but the client size of the form is now the same size as the source control, which is exactly what was desired. We still have the problem with the title bar and borders showing, and the positioning of the form needs to be fixed.

Figure 8. (1) View after the form has been resized; (2) view after the title bar and borders have been excluded

The Region property allows you to define the area of the window where the operating system permits drawing. This gives us the ability to dictate which parts of the window get drawn. Here, we're only interested in having Windows paint the client area of the form. This Size parameter (recall that this is expected to be a representation of the desired client area) is used in conjunction with a Point object containing the point where the title bar and left border end to create a Rectangle object representing the new region. This is then wrapped in a Region object, giving us the borderless view seen in the second image in Figure 8 above.

internal class DragForm : Form {

  internal void ChangeSize( Size newSize ) {

    ...

    this.Region = new Region( new Rectangle( new Point( 3, 19 ), newSize ) );

  }

}

Next, the problem with the positioning needs to be fixed. Even though we have created a new region, and the title bar and borders are no longer visible, the coordinates of the form are still relative to the upper left corner of the window. Since only the client area of the form is painted, we need to make some modifications so that the location of the form is relative to the client area. In other words, instead of the view in the second image in Figure 8, we want something like what is seen in Figure 9.

Figure 9. View after the Location property has been adjusted

The UpdateLocation method was added to make the deductions necessary to position the form's client area exactly above the source control when it's clicked. It has a Point parameter, which is expected to be the desired location of the form relative to the upper left corner of the client area. The x and y coordinates of the parameter are deducted from the left border width and title bar height, respectively. The updated coordinates are wrapped up inside a new Point object and its reference is passed onto the form's Location property.

internal class DragForm : Form {

  internal void UpdateLocation( Point newLocation ) {
    this.Location = new Point( newLocation.X - 3, newLocation.Y - 19 ); 
  }

}

When the user clicks on the source control, the coordinate differences between the cursor location and the client area of the DragForm instance are recorded through the CursorXDifference and CursorYDifference properties.

Figure 10. Demonstrates how the cursor difference property values are determined

This allows us to maintain consistency in the location of the form relative to the cursor position as the drag-and-drop operation is taking place.

internal class DragForm : Form {

  internal int CursorXDifference {
    get { return m_cursorXDifference; }
    set { m_cursorXDifference = value; }
  }

  internal int CursorYDifference {
    get { return m_cursorYDifference; }
    set { m_cursorYDifference = value; }
  }

}

The OnPaint method is overridden to draw a thin black border around the outer region of the form. This is done through the DrawRectangle method of the Graphics object. Note that when painting on a form, the coordinates are always relative to the upper left corner of the client area, regardless of whether or not the region has been modified. The client rectangle size is retrieved through the m_clientRectangleSize field, which contains a copy of the Size parameter that is passed into the ChangeSize method.

internal class DragForm : Form {

  protected override void OnPaint( PaintEventArgs e ) {

    e.Graphics.DrawRectangle( Pens.Black, 
      new Rectangle( 
        new Point( 0, 0 ), 
        new Size( m_clientRectangleSize.Width - 1, m_clientRectangleSize.Height - 1 ) ) );           

  }

}

Whenever you move the mouse cursor across the active window (remember that controls are considered child windows), or press or release one of the mouse buttons, Windows sends the WM_NCHITTEST message to query where the mouse cursor lies within the window. When the window's message queue processes this message, it responds with a hit test value (HT*) letting the system know what type of cursor to display and what will happen when the mouse is clicked or released. For instance, when the cursor is along the right edge of a form, the system receives the HTRIGHT value in response, which results in the cursor type being changed to Cursors.SizeWE.

When the drag-and-drop operation is taking place, the cursor will be consistently positioned above the dragged form, which means that the form's message queue will process all the WM_NCHITTEST messages. This also means that the necessary drag events in the target control will not get raised even if the cursor is above the control. Overriding the WndProc method and setting the message's Result property to HTTRANSPARENT tells the system to send the WM_NCHHITTEST message to the underlying control, ensuring that the necessary mouse events are raised. All the other messages are processed by the form.

internal class DragForm : Form {

  protected override void WndProc(ref Message m) {

    if ( m.Msg == WM_NCHITTEST ) {
      m.Result = ( IntPtr ) HTTRANSPARENT;
    } else {
      base.WndProc(ref m);
    }

  }

}

That's all there is to DragForm. It didn't take much code, but it certainly shows that it helps to know a few tricks. Let's jump right into incorporating it into the drag-and-drop experience.

As with every drag-and-drop operation, there is a source and a target. In this example, the source and target are PictureBox objects, named pictureBox1 and pictureBox2 respectively. The whole drag-and-drop experience starts when the user presses the mouse button above the source picture box.

In the MouseDown event handler, the DragForm gets the attention first. Its background color is changed to match the background color of the picture box, and then all of the other properties and methods that were recently discussed are updated or invoked. The drag is initiated by invoking the DoDragDrop method on the source object. The Color object that the BackColor property of the source PictureBox refers to is conveyed as the IDataObject parameter in the method invocation. Since the objective is to copy the color object from the source to the target, the DragDropEffects parameter is set to DragDropEffects.Copy.

public class Panel : UserControl {

  private void pictureBox1_MouseDown( object sender, System.Windows.Forms.MouseEventArgs e ) {

    if ( e.Button == MouseButtons.Left ) {

      df.BackColor = pictureBox1.BackColor;
      df.UpdateLocation( this.PointToScreen( pictureBox1.Location ) );
      df.CursorXDifference =  Cursor.Position.X - df.Location.X;
      df.CursorYDifference = Cursor.Position.Y - df.Location.Y;
      df.ChangeSize( pictureBox1.Size );
      df.ShowForm();
      
      pictureBox1.DoDragDrop( pictureBox1.BackColor, DragDropEffects.Copy );

    }

  }

}

Because I want to display the hand cursor at all times during the drag-and-drop operation, I need to subscribe to the source control's GetFeedback event. The GiveFeedbackEventHandler delegate contains a reference to a GiveFeedbackEventArgs object that exposes a UseDefaultCursor property which, when set to false, allows you to override the default drag-and-drop cursors and define your own.

public class Panel : UserControl {

  private void pictureBox1_GiveFeedback(object sender, GiveFeedbackEventArgs e) {

    e.UseDefaultCursors = false;
    Cursor.Current = Cursors.Hand;

  }

}

The QueryContinueDrag event is continually raised throughout the drag-and-drop operation. Handling this event enables me to check up on the status of the drag-and-drop operation. The QueryContinueDragEventHandler delegate provides a reference to an instance of QueryContinueDragEventArgs, which has an Action property that notifies you of the most recent drag action. There are three different types of actions in the DragAction enumeration: Cancel, Continue, and Drop. If the drag action is continue, the cursor position is checked and the location of the form is updated through its Location property. Otherwise, if the drag has been terminated either by a cancel or drop action, the form is hidden.

public class Panel : UserControl {

  private void pictureBox1_QueryContinueDrag(object sender, QueryContinueDragEventArgs e) {

    if ( e.Action == DragAction.Cancel || e.Action == DragAction.Drop ) {
      df.Hide();         
    } else {
    
      df.Location = new Point( Cursor.Position.X - df.CursorXDifference, 
        Cursor.Position.Y - df.CursorYDifference );         
  
    }

  }

}

The DragOver event is raised when the user drags the mouse cursor over a target control during a drag-and-drop operation. Its role is to determine whether or not the DragDrop event should be raised. If the Effect property of the DragEventArgs object is DragDropEffects.None, which is the default value, then the control will not accept the drop and the DragDrop event is not raised.

In the following implementation, a check is performed to see if the data associated with the drag-and-drop operation is of type Color. If it is, the DragDrop event is raised and the BackColor property of the target control is set to the color object.

public class Panel : UserControl {

  private void pictureBox2_DragOver( object sender, System.Windows.Forms.DragEventArgs e ) {
  
    if ( e.Data.GetData( typeof( Color ) ) != null ) {
      e.Effect = DragDropEffects.Copy;
    }

  }

  private void pictureBox2_DragDrop(object sender, System.Windows.Forms.DragEventArgs e) {

    Color c = ( Color ) e.Data.GetData( typeof( Color ) );
    pictureBox2.BackColor = c;

  }

}

Drag and Drop in ColorPicker.NET

The drag-and-drop operation in ColorPicker.NET is very similar to this example. The currently selected color window is the source control and the color swatch panel is the target. Users can build up custom color swatches by dragging colors from the source to the target. The only real difference in the application is the first available color swatch is highlighted when the DragOver event is fired to let the user know that they can drop the color. This can be seen in Figure 11.

Figure 11. Demonstrates the first available color swatch highlighted with a yellow border

Conclusion

This article provides some insight into the ColorPicker.NET infrastructure. I also demonstrate the techniques that were used to implement the color slider and the drag-and-drop functionality that were used in the application. I will be discussing some of the other components in a future article, but until then, the source code for the application is available if you're interested in taking a look at them.

Please note that ColorPicker.NET is a continually evolving project. The source code available with this article is a snapshot of code that was deemed stable at the time this article was written. For a more recent snapshot and/or the latest binaries, please go to the official ColorPicker.NET Web site at http://sano.dotnetgeeks.net/colorpicker.

 

About the Author

Chris Sano is a Software Design Engineer with MSDN. When he's not working like mad on his next article, he can be found wreaking havoc with his hockey stick at the local ice rink. He welcomes you to contact him at csano@microsoft.com with any questions or comments that you may have about this article, or things that you'd like to see in future pieces.