Custom Windows Forms Controls: ColorPicker.NET, Part 2

 

Chris Sano
Microsoft Corporation

March 2006

Applies to:
   Microsoft .NET Framework
   Windows Forms
   Custom Controls
   ColorPicker.NET

Summary: Chris Sano continues his introduction to ColorPicker.NET and demonstrates some more techniques that were used to build some of the custom controls in the application. (26 printed pages)

Click here to download ColorPicker.msi.

Contents

Introduction
The Magnifier Control
Using the Control
Conclusion
Acknowledgements

Introduction

A few months ago, I introduced you to one of my personal projects, ColorPicker.NET. Since then, the tool has evolved into a multi-purpose color picker that not only allows you to sample colors from two different color spaces (RGB and HSB), but also enables you to capture different portions of your desktop, zoom in on the captured region, and make pixel-specific color selections.

Figure 1 shows a screen shot of the new screen picker tab in the tool. On the left side, marker 1 identifies the screen capture control. On each of the four sides, there are translucent rectangles adorned with arrowheads pointing towards the outer regions of the control. Those are scrolling hotspots that, when activated, scroll the captured region across the desktop in the chosen direction.

Figure 1. The screen picker tab page in ColorPicker.NET

By clicking anywhere within the control not containing the hotspots, the zooming box will be activated. The magnifier control, indicated by marker 2, is used to define the desired zoom power level. There are six different levels from which the user can choose.

The magnified image will show up in the zoomed image control region in the top right area of the tab page (marker 3). Colors can be sampled by holding down the left mouse button within this area. The RGB, HSB, and hexadecimal values will be displayed in the bottom part of the panel along with an enlarged representation of the selected color.

I have two goals with this article. The first is to walk you through the creation of the magnifier control. We'll take a look at how the control is rendered and the logic required to make everything work, including the special animation effects that were conjured up to enhance the control. Secondly, I'll show you how the control was integrated into the tool and demonstrate how I was able to achieve the zooming functionality that drives the screen capturing and sampling feature.

The Magnifier Control

I needed something that would allow users to change their preferred zoom power level. Instead of putting together an austere control comprising a label with text that instructed users to select the preferred zoom power level from a list of static values available through a combo box as manifested in Figure 2, I wanted to compose an alluring control that fit in with the overall theme of the screen capture feature.

Figure 2. Label and combo box

The first step in the creation of this control was to come up with the graphics that were to be used as visuals. Since this control was going to be used where there would be zooming functionality, I decided that the best graphical representation of this would be a magnifying glass along with a way to indicate how much the user wanted to zoom in or out. The magnifying glass and zoom power panel, seen in Figure 3, were fabricated in Adobe Photoshop.

Figure 3. (a) The magnifying glass and (b) zoom power panel

The two images were then separated to their own canvases and exported as PNG files. PNG files are distinct from their graphic compression counterparts (GIF, JPEG, and others) in providing full alpha channel support. When the file is saved, the PNG compression engine iterates over each pixel, defining the color and its opacity (in other words, preserving transparency) and stores this information in 8 or 16 bit chunks of contiguous memory. During the rendering process, the image is combined with a background image (or color) to create a composite image.

Figure 4 shows an example of a composite image in the context of the magnifier control. As you can see, the magnifying glass and zoom power panels have been combined with a grey color resembling the quintessential control background color to create what will be the overall appearance of the control.

Figure 4. The magnifier control

Notice how the glass portion of the magnifying glass has just enough transparency for the power level on the underlying zoom power panel to be seen. This was important because I wanted the magnifying glass to lie directly above (and eventually scroll across) each of the numerical values and not be intrusive while doing so. The transparency also provides an effect of slightly accentuating the numbers.

With the graphics complete, the next thing on the list was to add the image files to my project. Since I wanted to rid myself of any external file dependencies, I designated the files as embedded resource files. This relieved me from the burden of having to handle scenarios such as the one in which the user might inadvertently (or perhaps maliciously) delete one or both of the image files from the application directory.

As seen in the following code block, the Bitmap class has a constructor that provides a quick and easy way of retrieving bitmap resources from the assembly in which they are embedded.

public class MagnifierControl : Control {
   // private data fields
private Bitmap m_magnifierImage;
   private Bitmap m_zoomPowerImage;
...
protected override void OnLoad(EventArgs e) {
base.OnLoad (e);
      
m_zoomPowerImage = new Bitmap( this.GetType(), "Images.zoompower.png" );
      m_magnifierImage = new Bitmap( this.GetType(), "Images.magnifier.png" );
                        
   ...
}
}

The value of the second parameter to the constructor, known as the manifest resource name, might be slightly confusing. That's because at compile time, Microsoft Visual Studio precedes the file name with a period delimited string representing the directory hierarchy in which the file is located relative to the project directory. In the sample code, the images are located in an Images directory. To clarify, if the magnifier.png file was located in an Images\MagnifyingGlass directory inside of the project directory, then the manifest resource name would be Images.MagnifyingGlass.magnifier.png.

To learn more about managing embedded resources inside and outside of Visual Studio, I highly recommend Chris Sells' article on the basics of resources in the Microsoft .NET Framework.

When the images are loaded, their locations are defined and stored in Rectangle objects. This allows me to keep track of the image boundaries throughout the lifetime of the control.

public class MagnifierControl : Control {
// private data fields
   private Rectangle m_zoomPowerImageRect;
   private Rectangle m_magnifierImageRect;
...
protected override void OnLoad(EventArgs e) {
      ...
                        
   m_zoomPowerImageRect = new Rectangle( 
new Point( 31, 10 ), 
new Size( m_zoomPowerImage.Width, m_zoomPowerImage.Height ) ); 
   m_magnifierImageRect = new Rectangle( 
new Point( 0, 12 ), 
new Size( m_magnifierImage.Width, m_magnifierImage.Height ) );

   ...
}
}

Once I had the graphics situated on the control canvas, the next step was to define the behaviors for the control. Behavior definition is one of the most critical steps in designing controls, as this is what can make or break them. Controls are event driven components that rely on various forms of user input to trigger their inherent behaviors. In many cases, particularly those in which .NET controls are involved, behaviors are inherited from a base control and others are added, resulting in an expanded set of behavioral functionality.

When the user clicks on a control, those that subscribe to the mouse click-induced events such as MouseDown and MouseUp or override the methods that raise those events (OnMouseDown and OnMouseUp, respectively) are able to retrieve the position of the cursor at the time that the WM_NCHHITTEST message is processed by the control window. This does not give me a way to identify which of the six possible levels of magnification the user has clicked on, if any at all. In order to make this happen, I needed to define click regions, each of which would represent a boundary around its associated magnification level, as seen in Figure 5.

Figure 5. The click regions on the zoom power panel

I defined the ZoomPowerClickRegion structure to help me keep track of the click regions. The public interface contains two properties and a constructor. The Region property returns a Rectangle object representing the coordinate value and size of the click region. The ImageCoordinates property returns the point to which the upper left corner of the magnifying glass image is to be moved when the region is clicked.

public class MagnifierControl : Control {
   public struct ZoomPowerClickRegion {
      public Rectangle Region { ... }
      public Point ImageCoordinates { ... }
      public ZoomPowerClickRegion( Rectangle region, Point imageCoordinates ) { ... }
   } 
}

Figure 6 illustrates how the two properties of ZoomPowerClickRegion come into play. I know I haven't showed you how the control determines which region has been clicked, but for now, pretend that the user has clicked on the third power level. As a result of this selection, the magnifying glass image has moved to the position defined by the ImageCoordinates property of the ZoomPowerClickRegion representing the region that was selected.

Figure 6. The ZoomPowerClickRegion properties

The CreateClickRegions method in the MagnifierControl class is responsible for generating the necessary instances of ZoomPowerClickRegion. Since there are six different click regions, an array of six ZoomPowerClickRegion objects is created. Prior to the iterative construction of those instances, temporary variables are created and defined for the height of the regions (regionHeight), the spacing in between regions (spacing), the y-coordinate location of the first click region (currentY), the magnifying glass image y-coordinate location for the first click region (imageY), and the value by which the tracking y-coordinate values are to be incremented (incrementalValue).

public class MagnifierControl : Control {
// private data fields
private ZoomPowerClickRegion[] m_clickRegions;
...
protected override void OnLoad(EventArgs e) {
   
...
CreateClickRegions();
   
...
}
   private void CreateClickRegions() {
      // there are six click regions on the zoomPower graphic.
      m_clickRegions = new ZoomPowerClickRegion[ 6 ];
      int regionHeight = 14;
      int spacing = 7;
      int currentY = m_zoomPowerImageRect.Y + 10;
      int imageY = m_zoomPowerImageRect.Y + 2;
      int incrementalValue = regionHeight + spacing;
      for ( int i = 0; i < m_clickRegions.Length; i++ ) {
         
         m_clickRegions[i] = new ZoomPowerClickRegion(
            new Rectangle(   new Point( m_zoomPowerImageRect.X, currentY ),
               new Size( m_zoomPowerImageRect.Width, regionHeight ) ),
            new Point( m_zoomPowerImageRect.X, imageY ) );
         currentY += incrementalValue;
         imageY += incrementalValue;
      }
   }
}

During creation, the click region and magnifying glass image location are calculated and passed into the ZoomPowerClickRegion constructor.

With the click regions established, the next step was to get the control to react appropriately when the user clicked above one of the regions. The easiest way to do this was to expand the functionality in the OnMouseUp method to include a positional check. A temporary rectangle object is created (cursorRect) to represent the pixel at which the cursor coordinate resides. Each ZoomPowerClickRegion instance in the array that was populated in the CreateClickRegions method is iterated, and a test is performed to see if there is an intersection between the temporary cursor rectangle and the Rectangle exposed by the instance's Region property. If there is, the OnZoomPowerClick method is invoked, which raises the ZoomPowerClick event, notifying listeners that the zoom power level has changed.

public class MagnifierControl : Control {
// private data fields
private ZoomPowerClickRegion[] m_clickRegions;
...
   // events
public event ZoomPowerClickEventHandler ZoomPowerClick;
protected override void OnMouseUp( MouseEventArgs e ) {
   base.OnMouseUp( e );
   Rectangle cursorRect = new Rectangle( e.X, e.Y, 1, 1 );
   
   for ( int i=0; i < m_clickRegions.Length; i++ ) {
      
      if ( cursorRect.IntersectsWith( m_clickRegions[i].Region ) ) {
         
         OnZoomPowerClick( new ZoomPowerEventArgs( ( ZoomPower ) i ) );
         break;
      }
   }
}
}

At this point, when the user clicks on a zoom power level, the magnifying glass is relocated to the coordinates revealed by the ImageCoordinates property associated with the clicked region. So, if I clicked on 2x and the previously selected zoom power level was 5x, the magnifying glass would be relocated to the ImageCoordinate point value for the click region associated with the 2x zoom level, as seen in Figure 7.

Figure 7. Selecting a new zoom level

Next, I decided that I wanted the magnifying glass to slide vertically across the zoom power panel when a new zoom power level was selected. Figure 8 shows a visual rendition of a timed sequence of the target behavior, which consists of the magnifying glass scrolling from the bottom of the zoom power panel towards the location of the selected zoom level.

Figure 8. The magnifying glass image scrolling from the previously selected level of magnification (6x) to the new level (1x)

To make this animation occur, I enlisted the help of a Timer. Some additional instructions were added to the OnMouseUp method to store the index of the currently selected click region (m_currentValueIndex) and the target y-coordinate (m_targetY) and start up the timer.

public class MagnifierControl : Control {
// private data fields
   private int m_currentValueIndex;
   private int m_targetY;
   ...
   // controls
   private System.Windows.Forms.Timer scrollTimer;
   ...
protected override void OnMouseUp( MouseEventArgs e ) {
   
   ...
   
for ( int i=0; i < m_clickRegions.Length; i++ ) {
      
      if ( cursorRect.IntersectsWith( m_clickRegions[i].Region ) ) {
         // start sliding
         m_currentValueIndex = i;
         m_targetY = m_clickRegions[i].ImageCoordinates.Y;
         if ( !scrollTimer.Enabled ) {
            scrollTimer.Start();
         }
         
         OnZoomPowerClick( new ZoomPowerEventArgs( ( ZoomPower ) i ) );
         break;
      }
   }
}
}

In the Tick event handler, the current magnifying glass region needed to be invalidated and the new location configured. The difference between the magnifying glass region and the target y-coordinate is calculated (diff) and then divided by three to get the amount of vertical pixels that the image should be moved (scrollValue). The scroll value is either added to or deducted from the y-coordinate of the magnifying glass region to create the new location. This is repeated every time the tick event is handled until the scroll value is 0. A substantially smaller scroll value with each evaluation presents an effect of the magnifying glass starting out by sliding rapidly then decreasing in speed until it clicks into place at its target.

public class MagnifierControl : Control {
// private data fields
   private Rectangle m_magnifierImageRegion;
   ...
private void scrollTimer_Tick(object sender, System.EventArgs e) {
   
   this.Invalidate( m_magnifierImageRegion );
   int diff = Math.Abs( m_magnifierImageRegion.Y - m_targetY );         
   int scrollValue = ( int ) Math.Round( ( double ) diff / 3 );
   
   if ( scrollValue == 0 ) {
      m_magnifierImageRegion.Y = m_targetY;
      scrollTimer.Stop();
   } else {
      if ( m_magnifierImageRegion.Y < m_targetY ) {
         m_magnifierImageRegion.Y += scrollValue;
      } else {
         m_magnifierImageRegion.Y -= scrollValue;
      }
   } 
   this.Invalidate( m_magnifierImageRegion );
   
}
}

The magnifier control has been substantially enhanced with an animated sliding effect that is triggered when the user selects a new zoom power level. To make the control a fully interactive experience, I decided that I wanted to implement functionality that would clearly indicate to the user that they have entered what I call the active region of the control, indicated by the red marking in Figure 9.

Figure 9. Active region

The goal was to have the magnifying glass image work its way towards the cursor when it entered this region. When the cursor left the region, the image was to return to its default position, which would be the last click region that the user chose. Figure 10 shows a timed sequence of the magnifying glass image sliding towards the cursor as it enters the active region and sliding back to its initial position when the cursor exits.

Figure 10. The magnifying glass image (a) sliding towards the cursor and (b) sliding back to the most recently selected level of magnification after the cursor has left the active region

Because the active region strays from the normal rectangular region, defining a rectangle and performing an intersection test to determine whether or not the current cursor position was within the active region wouldn't have worked. Instead, I created an instance of GraphicsPath, a class from the System.Drawing.Drawing2D namespace that provides functionality to construct a series of connected lines and curves. The following block of code demonstrates how I created a path around the outer boundaries of the active region.

public class MagnifierControl : Control {
  private GraphicsPath m_activeRegion;
   
  public MagnifierControl() {
      
    ...
    
    m_activeRegion = new GraphicsPath( FillMode.Winding );
      
    // add top circle region to GraphicsPath
    m_activeRegion.AddEllipse( m_zoomPowerImageRect.X, m_zoomPowerImageRect.Y,
      m_zoomPowerImageRect.Width, m_zoomPowerImageRect.Width );
    // add bottom circle region to GraphicsPath
    m_activeRegion.AddEllipse( m_zoomPowerImageRect.X,
      m_zoomPowerImageRect.Bottom - m_zoomPowerImageRect.Width, m_zoomPowerImageRect.Width,
      m_zoomPowerImageRect.Width );
      
    // add center region to GraphicsPath
    m_activeRegion.AddRectangle( 
      new Rectangle( new Point( m_clickRegions[0].Region.X, m_clickRegions[0].Region.Y ), 
      new Size( m_zoomPowerImage.Width, 120 ) ) );
  
  }
}

I want to quickly point out that when instantiating the GraphicsPath object, I set its fill mode to the Winding mode. This ensures that subtraction does not occur when the path comprises multiple overlapping figures as it would have had the default Alternate mode been used. The differences in overlap between the two modes are better represented by Figure 11.

Figure 11. A fill of the active region path with the fill mode set to (a) Winding (b) Alternate

The OnMouseMove method was overridden to add the boundary check. This is done through the IsVisible method of the GraphicsPath object, which returns a Boolean value that indicates whether or not the given point is contained within the path defined by the object.

public class MagnifierControl : Control {
  protected override void OnMouseMove(MouseEventArgs e) {
    if ( m_activeRegion.IsVisible( new Point( e.X, e.Y ) ) ) {
      ...
    } else { ... }
  }   
}

If it is determined that the cursor coordinate point falls within the bounds of the active region of the zoom power panel, the cursor image is changed to a hand and the target value is set to the current y-coordinate of the cursor. If the cursor is outside of the active region, the target value is set to the y-coordinate of the Point object returned by the ImageCoordinate property of the ZoomPowerClickRegion object. Finally, if necessary, the timer object is started up.

public class MagnifierControl : Control {
  // private data fields
  private int m_currentValueIndex;
  private int m_targetY;
  ...
  protected override void OnMouseMove(MouseEventArgs e) {
    base.OnMouseMove (e);
    Rectangle cursorRect = new Rectangle( e.X, e.Y, 1, 1 );
    if ( this.IsPointWithinDesignatedScrollRegion( cursorRect ) ) {
         
      this.Cursor = Cursors.Hand;
      m_targetY = e.Y - TOP_VALUE;   
    } else {
      m_targetY = m_clickRegions[ this.m_currentValueIndex ].ImageCoordinates.Y;      
      this.Cursor = Cursors.Default;
    }
    if ( !scrollTimer.Enabled ) {
      scrollTimer.Start();         
    }
  }   
}

Figure 12 reveals an interesting problem that I ran into while testing to see how well the control was reacting to various cursor positions. When I would move the cursor quickly from the active region to a location outside of the control's outer boundaries, represented by the red outline, the magnifying glass image would continue to move towards the target location that was set in the OnMouseMove method.

Figure 12. (a) The magnifying glass image sliding towards the cursor and (b) continuing to slide after quickly moving the cursor outside of the control's bounds

I realized that this was occurring because I was moving the cursor so quickly that the expected WM_NCHITTEST message was not getting generated and routed to the control. Because of this, the OnMouseMove method wasn't getting invoked and the target coordinate was not getting set to the appropriate fallback value. Overriding the behavior of the OnMouseLeave method and establishing the correct target value using the index of the most recently selected zoom power level, as seen in the following code sample, fixed this problem.

public class MagnifierControl : Control {
  protected override void OnMouseLeave(EventArgs e) {
    
    base.OnMouseLeave (e);
            
    m_targetY = m_clickRegions[ this.m_currentValueIndex ].ImageCoordinates.Y;
    if ( !scrollTimer.Enabled ) {
      scrollTimer.Start();
    }
  }
}

As you will see in the next section, using the control is as easy as dropping it onto your form (or control) and subscribing to its ZoomPowerClick event so you can receive notification of any changes in the preferred level of magnification.

Using the Control

There were several problems that needed to be solved in order to successfully orchestrate the functionality present in the screen capture and sampling feature in the most recent release of ColorPicker.NET at the time of publication. One of those involved figuring out how to allow users to establish the preferred level of magnification, which was solved with the creation of the magnifier control. The second required the provision of a mechanism that would allow them to magnify a chosen region and eventually be able to choose a pixel from which a color would be calculated. In this section, I will dive towards the bottom of the implementation details of the prototype shown in Figure 13.

Figure 13. Screenshot of the prototype.

The components of the prototype are very similar to those that can be seen in Figure 1. By now, the magnifier control should no longer be a mystery to your optical nerves. The picture box marked with the Original Image text is parallel to the screen capture control (1) and the one marked with the Zoomed Region text is congruent with the zoomed image control (3).

The Original Image picture box has been customized. If you click within its boundaries, the mouse cursor will be replaced with a red rectangular border representing the chosen level of magnification. This results in the projection of a magnified reflection of the region encapsulated by the red rectangle to the Zoomed Region picture box. This experience persists as you move the cursor around, so long as you keep the left mouse button in a depressed state.

Under the covers, the logic is fairly simple, with much of the complexity encapsulated within the functionality driving the magnification. Before I get into that, I want to highlight some of the basic functionality in the prototype that leads up to the magnification behavior.

The form listens for the ZoomPowerClick event of the magnifier control. When the event is raised and handled, the Original Image picture box is told of the change through its ZoomPower property so that it can display the appropriately sized zoom rectangle when called upon to do so. This property accepts a value from the ZoomPower enumeration, which contains all of the possible zoom power levels. It then passes this value into the GetZoomSize method, which calculates and returns the zoom rectangle size.

public class DemoPictureBox : PictureBox {
// private data fields
   private ZoomPower m_zoomPower;
   private Size m_zoomRectangleSize;
   ...
   public ZoomPower ZoomPower {
      
      get { return m_zoomPower; }
      set {
      
         m_zoomPower = value; 
         m_zoomRectangleSize = GetZoomSize( m_zoomPower );   
      }
   }
   private Size GetZoomSize( ZoomPower zoomPower ) {
      
      int width = 0;
      int height = 0;
      switch ( zoomPower ) {
         
         case ZoomPower.One:
            width = 176;
            height= 128;
            break;
         case ZoomPower.Two:
            width = 88;
            height = 64;
            break;
         ...
      }
      return new Size( width, height );
   }
   
}

When the user depresses the left mouse button within the boundaries of the Original Image picture box, the zoom rectangle is displayed, as seen in Figure 14.

Figure 14. Portion of the Original Image picture box before and after the left mouse button is depressed

The behavior of the OnMouseDown and OnMouseMove methods include the invocation of the GetImageZoom helper method, which is responsible for generating the stretched image, wrapping it up in an ImageZoomEventArgs object, and propagating it to listeners of the ImageZoom event by way of the OnImageZoom method.

public class DemoPictureBox : PictureBox {
   protected override void OnMouseDown(MouseEventArgs e) {
      
      if ( e.Button == MouseButtons.Left ) {
         ...
         GetImageZoom();
         ...
      }
      
   }
   private void GetImageZoom() {
      Image img = ImageManipulation.StretchImage(
         this.BackgroundImage,
         new Point( m_zoomRectangle.X, m_zoomRectangle.Y ),
         m_zoomRectangle,
         new Size( 176, 128 ) );
      OnImageZoom( new ImageZoomEventArgs( img ) );
   }
}

When the GetImageZoom method is hit, the StretchImage method of a utility class, ImageManipulation, is invoked. This method is responsible for taking the region that is encapsulated by the zoom rectangle and stretching it to fit a given boundary; in this case, the Zoomed Region picture box.

Figure 15. The selected region of the Original Image picture box and the stretched image as shown in the Zoomed Image picture box

The implementation of the StretchImage method was fairly simple, but it does require a fundamental understanding of how drawing works in Windows. The next two paragraphs discuss the concept of device contexts in Windows and the abstraction that the .NET framework provides, all of which will hopefully make more sense as we go through the code.

In GDI, drawing operations are abstracted by device contexts. A device context is a GDI data structure that is maintained internally by Windows. It defines a set of graphical objects as well as the graphical modes that affect output relating to those objects. The simplest way to think of it is as a canvas that Windows gives you on which painting operations can be performed. You can select various GDI objects onto this canvas and execute drawing instructions that create the desired appearance.

The System.Drawing namespace in the framework provides yet another layer of abstraction on top of device contexts through the Graphics class. This removes the complexity of having to interact with the device contexts directly, especially when it comes to having to manage handles, selecting and unselecting objects to and from the device context, and making sure the appropriate deletion actions are taken on those unselected objects. Instead, you just create drawing objects and pass them to the Graphics object through one of its numerous methods; for example, creating a Pen object and dispatching it to the DrawRectangle method.

Let's look at the code. The method invoker must pass a reference to a Bitmap object that contains the region that is to be stretched. Also required are the zoom region, which as previously discussed is the selected region indicated by the red boundaries in the past three figures, and the desired size of the stretched region image.

public sealed class ImageManipulation {
   
   public static Bitmap StretchImage( Bitmap sourceImage, Rectangle zoomRegion, Size desiredSize ) {
   ...
}
}

In order to fulfill the contract that the method has with its caller, it must create and return a Bitmap object that contains the stretched image. The creation of the Bitmap object occurs first and is defined as a blank surface with its size proportional to the desiredSize parameter.

In Windows, you cannot draw directly on a Bitmap surface. If you wish to, you must create a memory device context, select the Bitmap object to it, and then perform the desired painting operations. As I mentioned earlier, this is abstracted away from you through the Graphics object, as seen in the following code block.

public sealed class ImageManipulation {
   
   public static Bitmap StretchImage( Bitmap sourceImage, Rectangle zoomRegion, Size desiredSize ) {
      Bitmap myImage = new Bitmap( desiredSize.Width, desiredSize.Height );
      using ( Graphics g = Graphics.FromImage( myImage ) ) {
...
}
   }
}

Now that the destination surface has been established, the next thing on the list is to transfer the bits over from the source image. The DrawImage method of the Graphics class provides an abstraction of the GDI32 StretchBlt function, which takes bits from the source device context and copies them to a defined region on the destination device context, stretching them to fit if necessary. The following code shows the invocation of the DrawImage method in which the source image, destination region, source region, and the unit of measure are defined.

public sealed class ImageManipulation {
   
   public static Bitmap StretchImage( Bitmap sourceImage, Rectangle zoomRegion, Size desiredSize ) {
      ...
      using ( Graphics g = Graphics.FromImage( myImage ) ) {
         g.InterpolationMode = InterpolationMode.NearestNeighbor;
   g.DrawImage( sourceImage, 
      new Rectangle( new Point( 0 ), desiredSize ),
      zoomRegion,
      GraphicsUnit.Pixel );
      }
      
      ...
   
}
}

The InterpolationMode property of the Graphics object was set to the NearestNeighbor enumeration value to allow for a more pixilated outcome, which is better represented by Figure 16. This affords the user the opportunity to make optimal pixel selections when performing color sampling.

Figure 16. Difference between using the (a) default interpolation mode and (b) the nearest-neighbor interpolation mode

If you're interested in taking a closer look at the code that was discussed in this section, the prototype solution is available for download with this article.

Conclusion

This article provided you with deep insight into what was involved in the design of the magnifier control. I demonstrated how I went from painting two discrete images on the surface of a control to creating an interactive experience in which the magnifying glass image reacted to user interaction with the zoom power panel. Towards the end of the article, I walked through a prototype that demonstrated how the control works in unison with other controls to afford the screen color sampling experience in ColorPicker.NET.

As always, latest source code and binaries for ColorPicker.NET are available for download at http://sano.dotnetgeeks.net/colorpicker.

Acknowledgements

Special thanks to Bob Powell, Bill Sempf, and Michael Weinhardt for the invaluable feedback they provided as I was working on this article.

 

About the author

Chris Sano is a Software Design Engineer with Server Feedback Systems, one of the many groups under Windows Server. He writes code during the day and spends evenings donning his secret superhero outfit and clashing with the evil villains who dare to attempt to wreak havoc in the city of Seattle.