Entries tagged with 'check'

Creating a scrollable and zoomable image viewer in C# Part 3

After part 2 added scrolling support, we are now going to extend this to support keyboard scrolling and panning with the mouse.

Design support

In order to enable panning, we're going to add three new properties. The AutoPan property will control if the user can click and drag the image with the mouse in order to scroll. Also, we'll add an InvertMouse property to control how the scrolling works. Finally the IsPanning property; however it can only be read publically, not set.

As well as the backing events for the above properties, we'll also add extra events - PanStart and PanEnd The normal Scroll event will be utilized while panning is in progress rather than a custom event.

Mouse Panning

To pan with the mouse, the user needs to "grab" the control by clicking and holding down the left mouse button. As they move the mouse, the control should automatically scroll in the opposite direction the mouse is moving (or if InvertMouse is set, in the same direction). Once the button is released, scrolling should stop.

We'll implement this by overriding OnMouseMove and OnMouseUp, shown below.

protected override void OnMouseMove(MouseEventArgs e)
{
  base.OnMouseMove(e);

  if (e.Button == MouseButtons.Left && this.AutoPan && this.Image != null)
  {
    if (!this.IsPanning)
    {
      _startMousePosition = e.Location;
      this.IsPanning = true;
    }

    if (this.IsPanning)
    {
      int x;
      int y;
      Point position;

      if (!this.InvertMouse)
      {
        x = -_startScrollPosition.X + (_startMousePosition.X - e.Location.X);
        y = -_startScrollPosition.Y + (_startMousePosition.Y - e.Location.Y);
      }
      else
      {
        x = -(_startScrollPosition.X + (_startMousePosition.X - e.Location.X));
        y = -(_startScrollPosition.Y + (_startMousePosition.Y - e.Location.Y));
      }

      position = new Point(x, y);

      this.UpdateScrollPosition(position);
    }
  }
}

protected override void OnMouseUp(MouseEventArgs e)
{
  base.OnMouseUp(e);

  if (this.IsPanning)
    this.IsPanning = false;
}

protected virtual void UpdateScrollPosition(Point position)
{
  this.AutoScrollPosition = position;
  this.Invalidate();
  this.OnScroll(new ScrollEventArgs(ScrollEventType.ThumbPosition, 0));
}

UpdateScrollPosition is a common method to set the viewport and refresh the control. The IsPanning property is used to notify the control internally that a pan operation has been started. It will also set a semi-appropriate cursor (we'll look at custom cursors another time), and raise either the PanStart or PanEnd events.

[DefaultValue(false), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden), Browsable(false)]
public bool IsPanning
{
  get { return _isPanning; }
  protected set
  {
    if (_isPanning != value)
    {
      _isPanning = value;
      _startScrollPosition = this.AutoScrollPosition;

      if (value)
      {
        this.Cursor = Cursors.SizeAll;
        this.OnPanStart(EventArgs.Empty);
      }
      else
      {
        this.Cursor = Cursors.Default;
        this.OnPanEnd(EventArgs.Empty);
      }
    }
  }
}

Keyboard Scrolling

The first two versions of this component effectively disabled keyboard support via the ControlStyles.Selectable control style and TabStop property. However, we now want to allow keyboard support. So the first thing we do is remove the call to disable the selectable style and resetting of the tab stop property from the constructor. We also remove the custom TabStop property we had implemented for attribute overriding.

With this done, we can now add some keyboard support. As the ScrollableControl doesn't natively support this, we'll do it ourselves by overriding OnKeyDown. One of the initial drawbacks is that it won't always capture special keys, such as the arrow keys.

In order for it to do so we need to let the control know that such keys are required by overriding IsInputKey - if this returns true, then the specified key is required and will be captured in OnKeyDown.

protected override bool IsInputKey(Keys keyData)
{
  bool result;

  if ((keyData & Keys.Right) == Keys.Right | (keyData & Keys.Left) == Keys.Left | (keyData & Keys.Up) == Keys.Up | (keyData & Keys.Down) == Keys.Down)
    result = true;
  else
    result = base.IsInputKey(keyData);

  return result;
}

protected override void OnKeyDown(KeyEventArgs e)
{
  base.OnKeyDown(e);

  switch (e.KeyCode)
  {
    case Keys.Left:
      this.AdjustScroll(-(e.Modifiers == Keys.None ? this.HorizontalScroll.SmallChange : this.HorizontalScroll.LargeChange), 0);
      break;
    case Keys.Right:
      this.AdjustScroll(e.Modifiers == Keys.None ? this.HorizontalScroll.SmallChange : this.HorizontalScroll.LargeChange, 0);
      break;
    case Keys.Up:
      this.AdjustScroll(0, -(e.Modifiers == Keys.None ? this.VerticalScroll.SmallChange : this.VerticalScroll.LargeChange));
      break;
    case Keys.Down:
      this.AdjustScroll(0, e.Modifiers == Keys.None ? this.VerticalScroll.SmallChange : this.VerticalScroll.LargeChange);
      break;
  }
}

protected virtual void AdjustScroll(int x, int y)
{
  Point scrollPosition;

  scrollPosition = new Point(this.HorizontalScroll.Value + x, this.VerticalScroll.Value + y);

  this.UpdateScrollPosition(scrollPosition);
}

When the left, right, up or down arrow keys are pressed, the control checks to see if a modifier such as shift or control is active. If not, then the control is scrolled either horizontally or vertically using the "small change" value of the appropriate scrollbar. If a modifier was set, then the scroll is made using the "large change" value.

The AdjustScroll method is used to "nudge" the scrollbars in the given direction, using values read from the HorizontalScroll and VerticalScroll - reading the AutoScrollPosition property didn't return appropriate results in our testing.

Sample Project

You can download the third sample project from the links below. The final article in the series will add autofit, centring and of course, zoom support.

Other articles in this series

Downloads:

  • imageboxsample-part3.zip

    (443.55 KB | 23 August 2010 )

    Sample project showing how to create an image viewer that supports scrolling with the mouse and keyboard, and panning with the mouse.

1 comment | | Trackback specific URL for this entry

Creating a scrollable and zoomable image viewer in C# Part 2

In the second part of our Creating a scrollable and zoomable image viewer in C# series we will update our component to support automatic scrolling when auto size is disabled and the image is larger than the client area of the control.

Setting up auto scrolling

Originally we inherited from Control, however this does not support automatic scrolling. Rather than reinventing the wheel at this point, we'll change the control to inherit from ScrollableControl instead. This will expose a number of new members, the ones we need are:

  • AutoScroll - Enables or disables automatic scrolling
  • AutoScrollMinSize - Specifies the minimum size before scrollbars appear
  • AutoScrollPosition - Specifies the current scroll position
  • OnScroll - Raised when the scroll position is changed

Using the above we can now offer full scrolling.

As the control will take care of the scrolling behaviour, we don't want the AutoScrollMinSize property to be available, so we'll declare a new version of it and hide it with attributes.

[Browsable(false), EditorBrowsable(EditorBrowsableState.Never), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
public new Size AutoScrollMainSize
{
  get { return base.AutoScrollMinSize; }
  set { base.AutoScrollMinSize = value; }
}

Initially the component only offered auto sizing and so we had defined an AdjustSize method which was called in response to various events and property changes. As we now need to set up the scrolling area if AutoScroll is enabled, this method is no longer as suitable. Instead, we add a pair of new methods, AdjustLayout and AdjustScrolling. Existing calls to AdjustSize are changed to call AdjustLayout instead, and this method now calls either AdjustScrolling or AdjustSize depending on the state of the AutoSize and AutoScroll properties.

The AdjustScrolling method is used to set the AutoScrollMainSize property. When this is correctly set, the ScrollableControl will automatically take care of displaying scrollbars.

protected virtual void AdjustLayout()
{
  if (this.AutoSize)
    this.AdjustSize();
  else if (this.AutoScroll)
    this.AdjustScrolling();
}

protected virtual void AdjustScrolling()
{
  if (this.AutoScroll && this.Image != null)
    this.AutoScrollMinSize = this.Image.Size;
}

Reacting to scroll changes

By overriding the OnScroll event we get notifications whenever the user scrolls the control, and can therefore redraw the image.

protected override void OnScroll(ScrollEventArgs se)
{
  this.Invalidate();

  base.OnScroll(se);
}

Painting adjustments

The initial version of our ImageBox tiled a bitmap across the client area of the control. In this new version, when we create the background tile, we now create a new TextureBrush. During drawing we can call FillRectangle and pass in the new brush and it will be tiled for us.

Another shortcoming of the first version was the borders. These were painted last, so that if the image was larger than the controls client area, the image wouldn't be painted on top of the borders. Now, the borders are drawn first and a clip region applied to prevent any overlap.

Finally of course, the position of the drawn image needs to reflect any scrollbar offset.

protected override void OnPaint(PaintEventArgs e)
{
  int borderOffset;
  Rectangle innerRectangle;

  borderOffset = this.GetBorderOffset();

  if (borderOffset != 0)
  {
    // draw the borders
    switch (this.BorderStyle)
    {
      case BorderStyle.FixedSingle:
        ControlPaint.DrawBorder(e.Graphics, this.ClientRectangle, this.ForeColor, ButtonBorderStyle.Solid);
        break;
      case BorderStyle.Fixed3D:
        ControlPaint.DrawBorder3D(e.Graphics, this.ClientRectangle, Border3DStyle.Sunken);
        break;
    }

    // clip the background so we don't overwrite the border
    innerRectangle = Rectangle.Inflate(this.ClientRectangle, -borderOffset, -borderOffset);
    e.Graphics.SetClip(innerRectangle);
  }
  else
    innerRectangle = this.ClientRectangle;

  // draw the background
  if (_texture != null && this.ShowGrid)
    e.Graphics.FillRectangle(_texture, innerRectangle);
  else
  {
    using (SolidBrush brush = new SolidBrush(this.BackColor))
      e.Graphics.FillRectangle(brush, innerRectangle);
  }

  // draw the image
  if (this.Image != null)
  {
    int left;
    int top;

    left = this.Padding.Left + borderOffset;
    top = this.Padding.Top + borderOffset;

    if (this.AutoScroll)
    {
      left += this.AutoScrollPosition.X;
      top += this.AutoScrollPosition.Y;
    }

    e.Graphics.DrawImageUnscaled(this.Image, new Point(left, top));
  }

  // reset the clipping
  if (borderOffset != 0)
    e.Graphics.ResetClip();
}

Sample Project

You can download the second sample project from the link below. The next article in the series will look at panning the image using the mouse within the client area of the image control.

Other articles in this series

Downloads:

  • imageboxsample-part2.zip

    (441.84 KB | 13 August 2010 )

    Second in a multi part series on creating an image viewer that can be scrolled and zoomed in C#. After part one created the initial component with auto resize, we now add scrolling support.

Post a Comment | | Trackback specific URL for this entry

Creating a scrollable and zoomable image viewer in C# Part 1

This is the first part in a series of articles that will result in a component for viewing an image. The final component will support zooming and scrolling.

In this first part, we're going to create a basic image viewer, without the scrolling and zooming. Rather than having a plain background however, we're going to create a two tone checker box effect which is often used for showing transparent images. We'll also allow this to be disabled and a solid colour used instead.

Creating the component

The component inherits from Control rather than something like PictureBox or Panel as we want to provide a lot of our own behaviour.

The first thing we'll do is override some properties - to hide the ones we won't be using such as Text and Font, and to modify others, such as making AutoSize visible, and changing the default value of BackColor.

Next is to add some new properties. We'll create the following properties and respective change events:

  • BorderStyle - A standard border style.
  • GridCellSize - The basic cell size.
  • GridColor and GridColorAlternate - The colors used to create the checkerboard style background.
  • GridScale - A property for scaling the GridCellSize for user interface options.
  • Image - The image to be displayed.
  • ShowGrid - Flag to determine if the checkerboard background should be displayed.

As we are offering auto size support, we also override some existing events so we can resize when certain actions occur, such as changing the control's padding or parent.

Setting control styles

As well as setting up default property values, the component's constructor also adjusts several control styles.

  • AllPaintingInWmPaint - We don't need a separate OnPaintBackground and OnPaint mechanism, OnPaint will do fine.
  • UserPaint - As we are doing entirely our own painting, we disable the base Control's painting.
  • OptimizedDoubleBuffer - Double buffering means the painting will occur in a memory buffer before being transferred to the screen, reducing flicker.
  • ResizeRedraw - Automatically redraw the component if it is resized.
  • Selectable - We disable this flag as we don't want the control to be receiving focus.
public ImageBox()
{
  InitializeComponent();

  this.SetStyle(ControlStyles.AllPaintingInWmPaint | ControlStyles.UserPaint | ControlStyles.OptimizedDoubleBuffer| ControlStyles.ResizeRedraw, true);
  this.SetStyle(ControlStyles.Selectable, false);
  this.UpdateStyles();

  this.BackColor = Color.White;
  this.TabStop = false;
  this.AutoSize = true;
  this.GridScale = ImageBoxGridScale.Small;
  this.ShowGrid = true;
  this.GridColor = Color.Gainsboro;
  this.GridColorAlternate = Color.White;
  this.GridCellSize = 8;
  this.BorderStyle = BorderStyle.FixedSingle;
}

Creating the background

The CreateGridTileImage method creates a tile of a 2x2 grid using many of the properties listed above which is then tiled across the background of the control.

protected virtual Bitmap CreateGridTileImage(int cellSize, Color firstColor, Color secondColor)
{
  Bitmap result;
  int width;
  int height;
  float scale;

  // rescale the cell size
  switch (this.GridScale)
  {
    case ImageBoxGridScale.Medium:
      scale = 1.5F;
      break;
    case ImageBoxGridScale.Large:
      scale = 2;
      break;
    default:
      scale = 1;
      break;
  }

  cellSize = (int)(cellSize * scale);

  // draw the tile
  width = cellSize * 2;
  height = cellSize * 2;
  result = new Bitmap(width, height);
  using (Graphics g = Graphics.FromImage(result))
  {
    using (SolidBrush brush = new SolidBrush(firstColor))
      g.FillRectangle(brush, new Rectangle(0, 0, width, height));

    using (SolidBrush brush = new SolidBrush(secondColor))
    {
      g.FillRectangle(brush, new Rectangle(0, 0, cellSize, cellSize));
      g.FillRectangle(brush, new Rectangle(cellSize, cellSize, cellSize, cellSize));
    }
  }

  return result;
}

Painting the control

As described above, we've disabled all default painting, so we simply need to override OnPaint and do our custom painting here.

protected override void OnPaint(PaintEventArgs e)
{
  if (_gridTile != null && this.ShowGrid)
  {
    // draw the background
    for (int x = 0; x < this.ClientSize.Width; x += _gridTile.Size.Width)
    {
      for (int y = 0; y < this.ClientSize.Height; y += _gridTile.Size.Height)
        e.Graphics.DrawImageUnscaled(_gridTile, x, y);
    }
  }
  else
  {
    using (SolidBrush brush = new SolidBrush(this.BackColor))
      e.Graphics.FillRectangle(brush, this.ClientRectangle);
  }

  // draw the image
  if (this.Image != null)
  {
    e.Graphics.DrawImageUnscaled(this.Image, new Point(this.Padding.Left + this.GetBorderOffset(), this.Padding.Top + this.GetBorderOffset()));
  }

  // draw the borders
  switch (this.BorderStyle)
  {
    case BorderStyle.FixedSingle:
      ControlPaint.DrawBorder(e.Graphics, this.ClientRectangle, this.ForeColor, ButtonBorderStyle.Solid);
      break;
    case BorderStyle.Fixed3D:
      ControlPaint.DrawBorder3D(e.Graphics, this.ClientRectangle, Border3DStyle.Sunken);
      break;
  }
}

First, we either draw a solid background using the BackColor property if ShowGrid is false, otherwise we tile the grid image created earlier.

Next we draw the actual image, if one has been set. The image is offset based on the border style and padding.

Finally we draw the border style to ensure it appears on top of the image if autosize is disabled and the control is too small.

Sample Project

You can download the first sample project from the links below. The next article in the series will look at implementing scrolling for when the image is larger than the display area of the control.

Other articles in this series

Downloads:

  • imageboxsample-part-1.zip

    (37.58 KB | 12 August 2010 )

    First in a multi part series on creating an image viewer that can be scrolled and zoomed in C#.

1 comment | | Trackback specific URL for this entry

Recent News

Recent Articles

Most Popular

Tags

Advertisments