This content has moved - please find it at https://devblog.cyotek.com.

Although these pages remain accessible, some content may not display correctly in future as the new blog evolves.

Visit https://devblog.cyotek.com.

Adding drag handles to an ImageBox to allow resizing of selection regions

The ImageBox control is already a versatile little control and I use it for all sorts of tasks. One of the features I recently wanted was to allow users to be able to select a source region, then adjust this as needed. The control already allows you to draw a selection region, but if you need to adjust that ... well, you can't. You can only draw a new region.

This article describes how to extend the ImageBox to include the ability to resize the selection region. A older demonstration which shows how to drag the selection around has also been incorporated, in a more tidy fashion than the demo.

The control in action - and yes, you can resize even when zoomed in or out

Note: The code presented in this article has not been added to the core ImageBox control. Mostly this is because I don't want to clutter the control with bloat (something users of the old PropertiesList control might wish I'd done!) and partly because I don't want to add changes to the control that I'll regret down the line - I don't need another mess like the Color Picker Controls where every update seems to be a breaking change! It most likely will be added to the core control after it's been dog-fooded for a while with different scenarios.

Getting Started

As I mentioned above, this isn't part of the core control (yet) and so has been added to a new ImageBoxEx control. Not the most imaginative of names, but with it's current status of internal demonstration code, it matters not.

In addition to this new sub-classed control, we also need some helper classes. First amongst these is a new enum to describe the drag handle anchors, so we know which edges to resize.

internal enum DragHandleAnchor
{
  None,
  TopLeft,
  TopCenter,
  TopRight,
  MiddleLeft,
  MiddleRight,
  BottomLeft,
  BottomCenter,
  BottomRight
}

Next we have the class that describes an individual drag handle - nothing special here, although I have added Enabled and Visible properties to allow for more advanced scenarios, such as locking an edge, or only showing some handles.

internal class DragHandle
{
  public DragHandle(DragHandleAnchor anchor)
    : this()
  {
    this.Anchor = anchor;
  }

  protected DragHandle()
  {
    this.Enabled = true;
    this.Visible = true;
  }

  public DragHandleAnchor Anchor { get; protected set; }

  public Rectangle Bounds { get; set; }

  public bool Enabled { get; set; }

  public bool Visible { get; set; }
}

While you probably wouldn't do this, hiding one or two of the drag handles could be useful for some scenarios

The final support class is a collection for our drag handle objects - we could just use a List<> or some other generic collection but as a rule it's best not to expose these in a public API (and this code will be just that eventually) so we'll create a dedicated read-only collection.

internal class DragHandleCollection : IEnumerable<DragHandle>
{
  private readonly IDictionary<DragHandleAnchor, DragHandle> _items;

  public DragHandleCollection()
  {
    _items = new Dictionary<DragHandleAnchor, DragHandle>();
    _items.Add(DragHandleAnchor.TopLeft, new DragHandle(DragHandleAnchor.TopLeft));
    _items.Add(DragHandleAnchor.TopCenter, new DragHandle(DragHandleAnchor.TopCenter));
    _items.Add(DragHandleAnchor.TopRight, new DragHandle(DragHandleAnchor.TopRight));
    _items.Add(DragHandleAnchor.MiddleLeft, new DragHandle(DragHandleAnchor.MiddleLeft));
    _items.Add(DragHandleAnchor.MiddleRight, new DragHandle(DragHandleAnchor.MiddleRight));
    _items.Add(DragHandleAnchor.BottomLeft, new DragHandle(DragHandleAnchor.BottomLeft));
    _items.Add(DragHandleAnchor.BottomCenter, new DragHandle(DragHandleAnchor.BottomCenter));
    _items.Add(DragHandleAnchor.BottomRight, new DragHandle(DragHandleAnchor.BottomRight));
  }

  public int Count
  {
    get { return _items.Count; }
  }

  public DragHandle this[DragHandleAnchor index]
  {
    get { return _items[index]; }
  }

  public IEnumerator<DragHandle> GetEnumerator()
  {
    return _items.Values.GetEnumerator();
  }

  public DragHandleAnchor HitTest(Point point)
  {
    DragHandleAnchor result;

    result = DragHandleAnchor.None;

    foreach (DragHandle handle in this)
    {
      if (handle.Visible && handle.Bounds.Contains(point))
      {
        result = handle.Anchor;
        break;
      }
    }

    return result;
  }

  IEnumerator IEnumerable.GetEnumerator()
  {
    return this.GetEnumerator();
  }
}

Again, there's not much special about this class. As it is a custom class it does give us more flexibility, such as initializing the required drag handles, and providing a convenient HitTest method so we can check if a given point is within the bounds of a DragHandle.

Positioning drag handles around the selection region

The ImageBox control includes a nice bunch of helper methods, such as PointToImage, GetOffsetRectangle and more, which are very useful for adding scalable elements to an ImageBox instance. Unfortunately, they are all virtually useless for the drag handle code due to the fact that the handles themselves must not scale - the positions of course must update and resizing must be accurate whether at 100% zoom or not, but the size must not. This means we can't rely on the built in methods and must manually recalculate the handles whenever the control changes.

private void PositionDragHandles()
{
  if (this.DragHandles != null && this.DragHandleSize > 0)
  {
    if (this.SelectionRegion.IsEmpty)
    {
      foreach (DragHandle handle in this.DragHandles)
      {
        handle.Bounds = Rectangle.Empty;
      }
    }
    else
    {
      int left;
      int top;
      int right;
      int bottom;
      int halfWidth;
      int halfHeight;
      int halfDragHandleSize;
      Rectangle viewport;
      int offsetX;
      int offsetY;

      viewport = this.GetImageViewPort();
      offsetX = viewport.Left + this.Padding.Left + this.AutoScrollPosition.X;
      offsetY = viewport.Top + this.Padding.Top + this.AutoScrollPosition.Y;
      halfDragHandleSize = this.DragHandleSize / 2;
      left = Convert.ToInt32((this.SelectionRegion.Left * this.ZoomFactor) + offsetX);
      top = Convert.ToInt32((this.SelectionRegion.Top * this.ZoomFactor) + offsetY);
      right = left + Convert.ToInt32(this.SelectionRegion.Width * this.ZoomFactor);
      bottom = top + Convert.ToInt32(this.SelectionRegion.Height * this.ZoomFactor);
      halfWidth = Convert.ToInt32(this.SelectionRegion.Width * this.ZoomFactor) / 2;
      halfHeight = Convert.ToInt32(this.SelectionRegion.Height * this.ZoomFactor) / 2;

      this.DragHandles[DragHandleAnchor.TopLeft].Bounds = new Rectangle(left - this.DragHandleSize, top - this.DragHandleSize, this.DragHandleSize, this.DragHandleSize);
      this.DragHandles[DragHandleAnchor.TopCenter].Bounds = new Rectangle(left + halfWidth - halfDragHandleSize, top - this.DragHandleSize, this.DragHandleSize, this.DragHandleSize);
      this.DragHandles[DragHandleAnchor.TopRight].Bounds = new Rectangle(right, top - this.DragHandleSize, this.DragHandleSize, this.DragHandleSize);
      this.DragHandles[DragHandleAnchor.MiddleLeft].Bounds = new Rectangle(left - this.DragHandleSize, top + halfHeight - halfDragHandleSize, this.DragHandleSize, this.DragHandleSize);
      this.DragHandles[DragHandleAnchor.MiddleRight].Bounds = new Rectangle(right, top + halfHeight - halfDragHandleSize, this.DragHandleSize, this.DragHandleSize);
      this.DragHandles[DragHandleAnchor.BottomLeft].Bounds = new Rectangle(left - this.DragHandleSize, bottom, this.DragHandleSize, this.DragHandleSize);
      this.DragHandles[DragHandleAnchor.BottomCenter].Bounds = new Rectangle(left + halfWidth - halfDragHandleSize, bottom, this.DragHandleSize, this.DragHandleSize);
      this.DragHandles[DragHandleAnchor.BottomRight].Bounds = new Rectangle(right, bottom, this.DragHandleSize, this.DragHandleSize);
    }
  }
}

The code is fairly straightforward, but we need to call it from a few places, so we have a bunch of overrides similar to the below.

protected override void OnScroll(ScrollEventArgs se)
{
  base.OnScroll(se);

  this.PositionDragHandles();
}

We call PositionDragHandles from the constructor, and the Scroll, SelectionRegionChanged, ZoomChanged and Resize events.

Painting the drag handles

Painting the handles is simple enough - after normal painting has occurred, we draw our handles on top.

protected override void OnPaint(PaintEventArgs e)
{
  base.OnPaint(e);

  if (this.AllowPainting && !this.SelectionRegion.IsEmpty)
  {
    foreach (DragHandle handle in this.DragHandles)
    {
      if (handle.Visible)
      {
        this.DrawDragHandle(e.Graphics, handle);
      }
    }
  }
}

protected virtual void DrawDragHandle(Graphics graphics, DragHandle handle)
{
  int left;
  int top;
  int width;
  int height;
  Pen outerPen;
  Brush innerBrush;

  left = handle.Bounds.Left;
  top = handle.Bounds.Top;
  width = handle.Bounds.Width;
  height = handle.Bounds.Height;

  if (handle.Enabled)
  {
    outerPen = SystemPens.WindowFrame;
    innerBrush = SystemBrushes.Window;
  }
  else
  {
    outerPen = SystemPens.ControlDark;
    innerBrush = SystemBrushes.Control;
  }

  graphics.FillRectangle(innerBrush, left + 1, top + 1, width - 2, height - 2);
  graphics.DrawLine(outerPen, left + 1, top, left + width - 2, top);
  graphics.DrawLine(outerPen, left, top + 1, left, top + height - 2);
  graphics.DrawLine(outerPen, left + 1, top + height - 1, left + width - 2, top + height - 1);
  graphics.DrawLine(outerPen, left + width - 1, top + 1, left + width - 1, top + height - 2);
}

Disabled drag handles are painted using different colors

Updating the cursor

As the mouse travels across the control, we need to adjust the cursor accordingly - either to change it to one of the four resize cursors if the mouse is over an enabled handle, or to the drag cursor if it's within the bounds of the selection region. Of course, we also need to reset it if none of these conditions are true.

private void SetCursor(Point point)
{
  Cursor cursor;

  if (this.IsSelecting)
  {
    cursor = Cursors.Default;
  }
  else
  {
    DragHandleAnchor handleAnchor;

    handleAnchor = this.IsResizing ? this.ResizeAnchor : this.HitTest(point);
    if (handleAnchor != DragHandleAnchor.None && this.DragHandles[handleAnchor].Enabled)
    {
      switch (handleAnchor)
      {
        case DragHandleAnchor.TopLeft:
        case DragHandleAnchor.BottomRight:
          cursor = Cursors.SizeNWSE;
          break;
        case DragHandleAnchor.TopCenter:
        case DragHandleAnchor.BottomCenter:
          cursor = Cursors.SizeNS;
          break;
        case DragHandleAnchor.TopRight:
        case DragHandleAnchor.BottomLeft:
          cursor = Cursors.SizeNESW;
          break;
        case DragHandleAnchor.MiddleLeft:
        case DragHandleAnchor.MiddleRight:
          cursor = Cursors.SizeWE;
          break;
        default:
          throw new ArgumentOutOfRangeException();
      }
    }
    else if (this.IsMoving || this.SelectionRegion.Contains(this.PointToImage(point)))
    {
      cursor = Cursors.SizeAll;
    }
    else
    {
      cursor = Cursors.Default;
    }
  }

  this.Cursor = cursor;
}

Initializing a move or a drag

When the user first presses the left mouse button, check to see if the cursor is within the bounds of the selection region, or any visible drag handle. If so, we record the location of the cursor, and it's offset to the upper left corner of the selection region.

The original cursor location will be used as the origin, so once the mouse starts moving, we use this to determine if a move should occur, or a resize, or nothing.

The offset is used purely for moving, so that we reposition the selection relative to the cursor position - otherwise it would snap to the cursor which would look pretty awful.

protected override void OnMouseDown(MouseEventArgs e)
{
  Point imagePoint;

  imagePoint = this.PointToImage(e.Location);

  if (e.Button == MouseButtons.Left && (this.SelectionRegion.Contains(imagePoint) || this.HitTest(e.Location) != DragHandleAnchor.None))
  {
    this.DragOrigin = e.Location;
    this.DragOriginOffset = new Point(imagePoint.X - (int)this.SelectionRegion.X, imagePoint.Y - (int)this.SelectionRegion.Y);
  }
  else
  {
    this.DragOriginOffset = Point.Empty;
    this.DragOrigin = Point.Empty;
  }

  base.OnMouseDown(e);
}

Even if the user immediately moves the mouse, we don't want to trigger a move or a resize - the mouse may have just twitched. Instead, we wait until it moves beyond an area centred around the drag origin - once it has, then we trigger the action.

This drag rectangle is determined via the SystemInformation.DragSize (MSDN) property.

During a mouse move, as well as triggering a move or resize, we also need to process any in-progress action, as well as update the cursor as described in the previous section.

private bool IsOutsideDragZone(Point location)
{
  Rectangle dragZone;
  int dragWidth;
  int dragHeight;

  dragWidth = SystemInformation.DragSize.Width;
  dragHeight = SystemInformation.DragSize.Height;
  dragZone = new Rectangle(this.DragOrigin.X - (dragWidth / 2), this.DragOrigin.Y - (dragHeight / 2), dragWidth, dragHeight);

  return !dragZone.Contains(location);
}

protected override void OnMouseMove(MouseEventArgs e)
{
  // start either a move or a resize operation
  if (!this.IsSelecting && !this.IsMoving && !this.IsResizing && e.Button == MouseButtons.Left && !this.DragOrigin.IsEmpty && this.IsOutsideDragZone(e.Location))
  {
    DragHandleAnchor anchor;

    anchor = this.HitTest(this.DragOrigin);

    if (anchor == DragHandleAnchor.None)
    {
      // move
      this.StartMove();
    }
    else if (this.DragHandles[anchor].Enabled && this.DragHandles[anchor].Visible)
    {
      // resize
      this.StartResize(anchor);
    }
  }

  // set the cursor
  this.SetCursor(e.Location);

  // perform operations
  this.ProcessSelectionMove(e.Location);
  this.ProcessSelectionResize(e.Location);

  base.OnMouseMove(e);
}

Although I'm not going to include the code here as this article is already very code heavy, the StartMove and StartResize methods simply set some internal flags describing the control state, and store a copy of the SelectionRegion property - I'll explain why towards the end of the article. They also raise events, both to allow the actions to be cancelled, or to allow the application to update the user interface in some fashion.

Performing the move

Moving the selection around

Performing the move is simple - we calculate the new position of the selection region according to the cursor position, and including the offset from the original drag for a smooth move.

We also check to ensure that the full bounds of the selection region fit within the controls client area, preventing the user from dragging out outside the bounds of the underlying image/virtual size.

private void ProcessSelectionMove(Point cursorPosition)
{
  if (this.IsMoving)
  {
    int x;
    int y;
    Point imagePoint;

    imagePoint = this.PointToImage(cursorPosition, true);

    x = Math.Max(0, imagePoint.X - this.DragOriginOffset.X);
    if (x + this.SelectionRegion.Width >= this.ViewSize.Width)
    {
      x = this.ViewSize.Width - (int)this.SelectionRegion.Width;
    }

    y = Math.Max(0, imagePoint.Y - this.DragOriginOffset.Y);
    if (y + this.SelectionRegion.Height >= this.ViewSize.Height)
    {
      y = this.ViewSize.Height - (int)this.SelectionRegion.Height;
    }

    this.SelectionRegion = new RectangleF(x, y, this.SelectionRegion.Width, this.SelectionRegion.Height);
  }
}

Performing the resize

Resizing the selection

The resize code is also reasonably straight forward. We decide which edges of the selection region we're going to adjust based on the drag handle. Next, we get the position of the cursor within the underlying view - snapped to fit within the bounds, so that you can't size the region outside the view.

The we just update the edges based on this calculation. However, we also ensure that the selection region is above a minimum size. Apart from the fact that if the drag handles overlap it's going to be impossible to size properly, you probably want to force some minimum size constraints.

private void ProcessSelectionResize(Point cursorPosition)
{
  if (this.IsResizing)
  {
    Point imagePosition;
    float left;
    float top;
    float right;
    float bottom;
    bool resizingTopEdge;
    bool resizingBottomEdge;
    bool resizingLeftEdge;
    bool resizingRightEdge;

    imagePosition = this.PointToImage(cursorPosition, true);

    // get the current selection
    left = this.SelectionRegion.Left;
    top = this.SelectionRegion.Top;
    right = this.SelectionRegion.Right;
    bottom = this.SelectionRegion.Bottom;

    // decide which edges we're resizing
    resizingTopEdge = this.ResizeAnchor >= DragHandleAnchor.TopLeft && this.ResizeAnchor <= DragHandleAnchor.TopRight;
    resizingBottomEdge = this.ResizeAnchor >= DragHandleAnchor.BottomLeft && this.ResizeAnchor <= DragHandleAnchor.BottomRight;
    resizingLeftEdge = this.ResizeAnchor == DragHandleAnchor.TopLeft || this.ResizeAnchor == DragHandleAnchor.MiddleLeft || this.ResizeAnchor == DragHandleAnchor.BottomLeft;
    resizingRightEdge = this.ResizeAnchor == DragHandleAnchor.TopRight || this.ResizeAnchor == DragHandleAnchor.MiddleRight || this.ResizeAnchor == DragHandleAnchor.BottomRight;

    // and resize!
    if (resizingTopEdge)
    {
      top = imagePosition.Y;
      if (bottom - top < this.MinimumSelectionSize.Height)
      {
        top = bottom - this.MinimumSelectionSize.Height;
      }
    }
    else if (resizingBottomEdge)
    {
      bottom = imagePosition.Y;
      if (bottom - top < this.MinimumSelectionSize.Height)
      {
        bottom = top + this.MinimumSelectionSize.Height;
      }
    }

    if (resizingLeftEdge)
    {
      left = imagePosition.X;
      if (right - left < this.MinimumSelectionSize.Width)
      {
        left = right - this.MinimumSelectionSize.Width;
      }
    }
    else if (resizingRightEdge)
    {
      right = imagePosition.X;
      if (right - left < this.MinimumSelectionSize.Width)
      {
        right = left + this.MinimumSelectionSize.Width;
      }
    }

    this.SelectionRegion = new RectangleF(left, top, right - left, bottom - top);
  }
}

Finalizing the move/resize operations

So far, we've used the MouseDown and MouseMove events to control the initializing and processing of the actions. Now, we've use the MouseUp event to finish things off - to reset flags that describe the control state, and to raise events.

protected override void OnMouseUp(MouseEventArgs e)
{
  if (this.IsMoving)
  {
    this.CompleteMove();
  }
  else if (this.IsResizing)
  {
    this.CompleteResize();
  }

  base.OnMouseUp(e);
}

Cancelling a move or resize operation

Assuming the user has started moving the region or resizes it, and then changes their mind. How to cancel? The easiest way is to press the Escape key - and so that's what we'll implement.

We can do this by overriding ProcessDialogKey, checking for Escape and then resetting the control state, and restoring the SelectionRegion property using the copy we started at the start of the operation.

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

  if (keyData == Keys.Escape && (this.IsResizing || this.IsMoving))
  {
    if (this.IsResizing)
    {
      this.CancelResize();
    }
    else
    {
      this.CancelMove();
    }

    result = true;
  }
  else
  {
    result = base.ProcessDialogKey(keyData);
  }

  return result;
}

Wrapping up

That covers most of the important code for making these techniques work, although it's incomplete, so please download the latest version for the full source. And I hope you find this addition to the ImageBox component useful!

Update History

  • 2014-02-13 - First published
  • 2020-11-21 - Updated formatting

Downloads

Filename Description Version Release Date
Cyotek.Windows.Forms.ImageBox.zip
  • md5: eafe88cd279eec36bc79f6409f0fc49d

Cyotek ImageBox Control, 1.1.4.2 update

1.1.4.2 13/02/2014 Download

About The Author

Gravatar

The founder of Cyotek, Richard enjoys creating new blog content for the site. Much more though, he likes to develop programs, and can often found writing reams of code. A long term gamer, he has aspirations in one day creating an epic video game. Until that time, he is mostly content with adding new bugs to WebCopy and the other Cyotek products.

Leave a Comment

While we appreciate comments from our users, please follow our posting guidelines. Have you tried the Cyotek Forums for support from Cyotek and the community?

Styling with Markdown is supported

Comments

Lacko

# Reply

Hi Richard,

First of all, many thanks for your amazing work on this control. It saved me a lot of work, definitely. Though, I facing a little problem... When I make a Graphics object for the underlaying Image, and drawing something on it (lines, for example, or pixels, when the control looses the focus, every line just disappear. Is that a normal behaviour which can be modified with some properties, or what is more probably,do I miss some point... In later case, what can I misunderstood? I do not using any custom painton or so, only catch mouse move ,mouse click and keydown events... (and using DockPanel suite)

Any idea which can point me in right direction?

Thanks in advance,

BR, Lacko

Gravatar

Richard Moss

# Reply

Hello,

These comments aren't really best placed for support requests, so you may wish to post on our forums, and include sample code if possible. How are you performing the painting? The fact it's happening when the control loses focus would normally strongly suggest that the control is being invalidated, and however you're doing your painting either isn't being called, or is being called in the wrong order.

You said you're creating a Graphics object which I wouldn't have thought you would ever need to do - just tap into the Paint event of the ImageBox (or OnPaint if you have created a custom control) and do your custom painting there. Basically, it sounds like you're doing your custom drawing outside of the ImageBox control's normal paint chain, so when the control is invalidating itself, your own code is unaware that it needs to be redone.

Hope this helps!

Regards; Richard Moss

Lacko

# Reply

Hi Richard,

This can we call quick answer :-) thank you! I should been happy to ask my question on forum, if I just found it... but not managed..

The scenario is in short: I have a bitmap, ca. 2000 pixel wide. I need to show about 200 pixel wide part of it, which I managed. I need to simple edit some pixels by mouse click, managed as well. (using indexed bitmaps) I have a List, which contains simple line numbers in the bitmap, and I have to mark a selected line on the imagebox without reflecting it in the origan bitmap. So, I created a Graphics object, drwing the line, but the line just shown up, and disappear. I realized, disappear because lost the focus. I do not use any Paint or OnPanint event. You mention that the control invalidated, when loose the focus. If it does, then I understand, why my lines disappear. But is there any way, to prevent the imageBox to be invalidated?

Thanks a lot.

Lacko

Gravatar

Richard Moss

# Reply

Hello,

You can't stop the ImageBox from invalidating itself, if you do then it won't update at all. For example, panning or zooming will invalidate the control, even just moving another window in front of the control will cause parts of it to be repainted, that's the way Windows works. The ImageBox isn't a VB6 AutoRedraw PictureBox :)

You will need to hook into the paint chain, otherwise it's going to be impossible to keep your custom drawing available. Try to ensure that any slow code is performed only once (for example selecting an item in the List you mentioned) and then hook the Paint event and perform your custom painting on the Graphics object passed to this event. Then you can be sure that your custom code will always be executed whenever the control paints itself, regardless of what caused the control to be invalidated.

Regards; Richard Moss

Lacko

# Reply

Hello Richard,

Thanks a lot to pointing me in the right direction! I think, i got the point. I just realised I was totally out in the green as I did the custom drawing totally wrong place, in the List's selection handler. ( in fact, that worked just fine with a simple pictureBox control so i took it given to be a right solution -> my wrong.) Now I understood, this was a wrong approach. I will implement the new approach tonight, but i'm confident it will working without any problem. Thanks again for the amazing control and for the exceptionally great help!

Regards,

Lacko

Gravatar

Richard Moss

# Reply

You're welcome, hope you find the control useful!

Regards; Richard Moss

Lacko

# Reply

Hello Richard,

Yes, I find it very usefull! Just to follow up, based on your directions, i was able to solve my problem, it was straigtforward. Now, my app. working nice with your control! Thanks a lot again!

Regards,

Lacko

Gravatar

Richard Moss

# Reply

Excellent!

Gravatar

wmjordan

# Reply

The line "cursor = Cursors.Default;" before "this.Cursor = cursor;" in SetCursor should be changed to "cursor = this.IsPanning ? this.Cursor : Cursors.Default;". Otherwise, when we turn on panning and use this dragging enabled control, the panning cursor would be changed to Default.

Also, I suggest that leaving the SetCursor method to be overridable or interceptable by providing a callback to be executeed in the SetCursor method. I am now writing an image viewer which enables the user to click somewhere on the image to perform some actions. I would love to set the cursor to some shape when the user hovers his mouse on a certain regions. The SetCursor method keep turning the cursor to Default which makes the cursor flickering between Default and my own style.

Gravatar

Richard Moss

# Reply

Hello,

Thanks for the feedback - I'll take a look at the cursor bug you mentioned. In regards to the suggestion for overriding cursor I'll also take a look at this when I get some free time. As a workaround if you don't want to modify the control yourself, you could try intercepting the WM_SETCURSOR message and doing some custom cursor processing there (although that's a more advanced topic, probably much easier just to modify the base ImageBox control to suit your needs!)

I've logged both of these in our internal tracker, but if you wish to provide further feedback it might be easier to use the forums - these comments are a bit limited for this sort of thing.

Thanks again for the feedback!

Regards; Richard Moss

Joe

# Reply

Hello Richard,

I want to thank you for this control too! I find it very usefull! You save me a lot of work in my program! :) One question do, is it possible to change the zoom to float, or why did you use int? (On my image viewer program the zoom is not perfectly fit to the window) Thanks a lot again!

Best Regards,

Joe

Gravatar

Richard Moss

# Reply

Hello,

Thanks for the comment, glad you enjoy the control!

The Zoom property is an int because I thought it was easier. A zoom level of 100% is 100. 500% is 500. 50% is 50. Easier than 1, 5 or 0.5 I would have thought! (And I didn't really think users would want to specify 50.75%). Behind the scenes it is converted into a double and that is the value used for all calculations.

Assuming you are embedding the source directly in your project, you could just change the Zoom property to be a float directly and then adjust the calculation in ZoomFactor to do something other than Zoom / 100. Or, if you are using the NuGet package or don't wish to modify the original source you could add a new float based zoom property and then override both Zoom and ZoomFactor to use your new property and everything else should just work.

However, I don't really recall having an issue where the int based system didn't work - if you have an example, can you either sent it via email, or post it on our forums for further investigation?

Thanks; Richard Moss

Gravatar

Joe

# Reply

Hi Richard!

Thank you for your quick answer!

I write a kind of image viewer program with some extra functions. Something like (the great :) ) irfanview, but much smaller project.

Basically I have pictures -e.g.: from a digital camera, or from my phone, around 8-12 megapixel-, and I want to display them, make sure it's fit to the desktop. So if it's bigger, then I need to use the zoomtofit function. If I have 2448x3264 pixel image, and my monitor is 1920x1080 (include window frame, menu, toolbar, status-bar), I end up with 28,49% of zoom.

If the zoom is 28, then I have 3-4 millimeter "frame" around my image. I like to have without it . :) I already solved the problem, based on your guidance. I really thank you for that! :)

However I had to make a little bit more modification to accomplish that (technically I had to replace almost all int to float), so it would be hard to post in the forum. Also I changed selection to mouse right click, because I use left to drag image (or the selected area if any).

Also the zoom with mouse wheel to z+mouse wheel, because I navigate with the mouse, loop in the folder to load images.

If you like to have the imageBox (+imageBoxEx) project with my modifications (based on Cyotek.Windows.Forms.ImageBox +.Demo, +.Design), or pictures over the problem, I more than happy to send you.

Thank you again,

Joe

Gravatar

Koepisch

# Reply

Hello Richard,

could you tell how i can use EMF files within the ImageBox? I want to test the zoom and pan function only. I'm not sure if i could make the changes by myself.

Thanks and regards Koepisch

Gravatar

Richard Moss

# Reply

Hello,

I just tested a EMF file I generated via Inkscape, and it seemed to work fine - I could zoom, pan, etc all file. Is there a specific issue you're having?

Regards; Richard Moss

Gravatar

Rodolfo

# Reply

Hi! Thank you for this amazint tool, It's been very helpful I need to make some minor modifications in the Imagebox and i can't find where!

I already limited the zoom just by altering the ZoomLevelCollection (I want zoom up to 200% only).

Now, I need to remove the scrollbars but keep the PAN capability (drag and PAN the image when zoomed).

Also, when i set the "AutoCenter" to false, I need to choose where the figure will be in the Imagebox. For example: I need one image to be on the top right, another one in the bottom Left...

Thank you

Gravatar

Richard Moss

# Reply

Hello - as you've posted a more detailed version of your question on our forums, I will answer it there

Regards;
Richard Moss

Gravatar

ali

# Reply

Hi Richard, first of all thanks for your great job on imagebox. I have learned lot on implementing my own image box based on your tutorials and source codes. I have a question and i hope you could help me with. I am loading binary files as 1bpp bitmap into imagebox. and i have no problem with little ones. I read a byte array and convert it to bitmap like this: Bitmap bmp = new Bitmap(width, height, PixelFormat.Format1bppIndexed); BitmapData bmpData = bmp.LockBits(new Rectangle(0, 0, bmp.Width, bmp.Height), ImageLockMode.WriteOnly, bmp.PixelFormat); Marshal.Copy(data, 0, bmpData.Scan0, data.Length); but when it comes to large files my problem begins. I want to deal with files in size of more than 10gb. obviously i can't load all of the file at once in to the memory, and i have to load part of the file that it need to be display on the imagebox. I want to know is there a tool/way to ease my problem?

Gravatar

Richard Moss

# Reply

Ali,

Thanks for the comment, glad you enjoyed the control. That sounds like an impressively large image file! I actually got asked something similar recently, and while I don't have a solution for either question, I have an idea for yours. Assuming the bitmap isn't run length encoded, you could read the bitmap header to get the dimensions from which you could use to set up a virtual mode ImageBox. When it's time to paint the box, you could then calculate the bits of the file you need to read, pull the data out, and build a small bitmap of that particular "view" to display. This is at least feasible, but probably really complicated to do. At least it's possible though as no compression is involved so you can directly grab the bytes from the file and split out the bits representing the pixels.

Sorry I can't be more specific, but this isn't something I've needed to do before so while I have a guess, I don't have a concrete solution. I haven't tried working with 1bpp pixel images so I'm not sure how the data is stored in each byte - but there's probably multiple pixels in each one.

Regards;
Richard Moss

Alessio

# Reply

Very Helpful and with a lot of functions