Quantcast
Channel: cyotek.com Blog Summary Feed
Viewing all 559 articles
Browse latest View live

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

$
0
0

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!

Downloads


ColorPicker Controls 1.0.4.0 Update

$
0
0

The ColorPicker Controls have been updated to version 1.0.4.0.

This is a fairly substantial update, with quite a few bug fixes and enhancements to the code.

What's next for the library?

I feel the code is starting to get a little bloated as the library is trying to serve two purposes - the primary purpose is for the selection of colours. However, it also provides the ability to load and save colour swatches in various formats, some of which has been the subject of posts on this blog.

Internally, we link to the individual files from the library in our core product assemblies - all the UI stuff is in Cyotek.Windows.Forms, and everything else is in Cyotek.Drawing. I think it would probably be a good idea to properly split up this library too. If you just want the UI, use one library, if you want the extended palette serialisation support, use the other. There is some overlap though so this will need to be considered a bit first.

Of course splitting the library is a massive breaking change. I think future versions of our open source libraries will change to use Semantic Versioning so that it will be much clearer when things are being broken. Of course, it would be preferable if breaking changes weren't introduced all the time! This is also why I haven't added a NuGet package yet, when you update a package you don't expect to have to change your source code too.

Changes and new features

  • Added new AdobePhotoShopColorSwatchSerializer serializer for reading and writing Adobe PhotoShop colour swatches (both version 1 and version 2)
  • You can now set the Columns property of a ColorGrid control to 0, which will then internally calculate columns based on the size of the control, the cell size, and the spacing. A new read-only ActualColumns property has been added which will allow you to get the real number of columns if required. The AutoSize behaviour has been changed so that only the vertical height of the control is adjusted when Columns is zero
  • Save Palette button in the ColorPickerDialog now obtains the serializer to use based on the selected filter index, allowing correct saving if multiple serializers use the same extension.
  • Added CanReadFrom method to IPaletteSerializer.
  • PaletteSerializer.GetSerializer now makes use of the above new method to access the relevant serializer rather than just matching extensions. This means if you have two serializers that support different .pal formatted files, these can now be loaded successfully, instead of one loading and one failing.
  • Added new RawPaletteSerializer which reads and writes palettes that are simply RGB triplets in byte form
  • Added new ShowAlphaChannel property to ColorEditor and ColorPickerDialog. This property allows the alpha channel editing controls to be hidden, for when working with 8-bit colours.
  • The rendering of the selected cell in a ColorGrid control who's SelectedCellStyle is Zoomed now uses Padding.Left and Padding.Top to determine the size of the zoom box, avoiding massive boxes the larger the CellSize gets.
  • Added a new standard 256 colour palette. You can use this in the ColorGrid by setting the Palette property to ColorPalette.Standard256 or obtain the array of colours by calling ColorPalettes.StandardPalette
  • ColorGrid and RgbaColorSlider controls now only create transparency brushes when required. A new virtual method SupportsTransparentBackColor allows inheritors to create their own brushes if required.
  • Added EditingColor event to ColorGrid, allowing the edit colour action to be cancelled, or replaced with a custom editor
  • Added CurrentCell property and GetCellOffset methods to the ColorGrid.
  • ColorCollection now implements IEquatable
  • Added more tests
  • Added new Navigate method to ColorGrid for easier moving within the cells of the grid

Bug Fixes

  • The ColorGrid control now tries to be smarter with painting, and only paints cells that intersect with the clip rectangle. In addition, where possible only individual cells are invalidated rather than the entire control.
  • Corrected invalid error messages from the Save Palette button in the ColorPickerDialog.
  • Load Palette and Save Palette buttons in the ColorPickerDialog now check the CanRead and CanWrite properties of the serializer.
  • Double clicking with any button other than the left in ColorGrid control no longer attempts to initiate colour editing
  • Setting the Color property of the ColorGrid control to Color.Empty no longer treats the value as a valid colour
  • The ColorGrid control no longer defines custom colour regions when the ShowCustomColors property was false. This manifested in hover and selection effects working if you moved your mouse over the bottom of a resized grid.
  • Clicking "white space" areas of a ColorWheel control will no longer incorrectly set the colour to the closest matching point on the wheel itself. However, starting to select a colour within the wheel and then moving outside the bounds will continue to select the closest match as usual.
  • Fixed a crash that occurred when creating controls that inherited from ColorGrid or RgbaColorSlider
  • When the AutoAddColors and ShowCustomColors properties are false, unmatched colours will no longer be silently added to the ColorGrid custom palette unexpectedly. This also resolves various crashes after the colour regions fix above was applied.
  • The ColorWheel control now makes use of ButtonRenderer.DrawParentBackground to draw itself, to avoid ugly blocks of solid colours when hosted in containers such as the TabControl
  • The ColorEditorManager control's ColorChanged event has now been marked as the default event, so when you double click the component in the designer, a code window now correctly opens.
  • If the underlying entry in a ColorCollection bound to a ColorGrid control was modified, and this particular entry was the selected colour, the ColorGrid would not keep its Color property in sync and would clear the selected index.
  • Attempting to set the Columns property to less than zero now throws an ArgumentOutOfRange exception rather than setting it, then crashing later on
  • Double clicking a colour in the grid of the ColorPickerDialog no longer opens another copy of the ColorPickerDialog
  • Fixed problems in the ColorGrid with keyboard navigation and initial focus if no valid colour index was set.
  • The ColorCollection.Find method now correctly works when adding named colours (e.g. Color.CornflowerBlue) to the collection, but searching by ARGB value (e.g. Color.FromArgb(100, 149, 237))
  • Fixed an issue where if the internal dictionary lookup in ColorCollection class had been created and the collection was then updated, in some cases the lookup wasn't correctly modified.

Add Projects Extension - 1.0.1.0

$
0
0

A short and sweet post today...

I've been happily using the Add Projects extension since first writing it several months ago, and I actually find it a real time saver.

However, one thing that has been bugging me is trying to find specific projects in an ever growing list.

I've just updated the extension to version 1.0.1.0 by adding a handy filter option to the main dialog (sorry, it's still Windows Forms as opposed to XAML, so continues to look clunky in the VSIDE!)

I've also pushed the source to GitHub. At some point I'll convert it to XAML and properly publish it, but for now the (signed) package can be downloaded here.

One word of caution - I doubt the source code project will open in Visual Studio 2012 any more, as I had to install the VS2013 SDK and upgrade the project to work on this update. The compiled extension is supported on Visual Studio 2012 and Visual Studio 2013.

MVC actions, AcceptVerbs, HEAD requests and 404 errors

$
0
0

When running Sitemap Creator on the development version of cyotek.com, we found all links pointing to articles returned a 404 status code when crawling was attempted. But if same URL was copied into a browser, it would load correctly.

This surprised us, as cyotek.com is the main site we test Sitemap Creator and WebCopy on and they've always worked in the past. Next, we tried it directly on cyotek.com, and got the same result. However, this being the release version of the web, we started receiving error emails from the website (these are not sent from the debug builds).

The exception being reported was this:

System.Web.HttpException: A public action method 'display' could not be found on controller 'Cyotek.Web.Controllers.ArticleController'. at System.Web.Mvc.Controller.HandleUnknownAction(String actionName) at System.Web.Mvc.Controller.ExecuteCore() at System.Web.Mvc.ControllerBase.Execute(RequestContext requestContext) at System.Web.Mvc.ControllerBase.System.Web.Mvc.IController.Execute(RequestContext requestContext) at System.Web.Mvc.MvcHandler.ProcessRequest(HttpContextBase httpContext) at System.Web.Mvc.MvcHandler.ProcessRequest(HttpContext httpContext) at System.Web.Mvc.MvcHandler.System.Web.IHttpHandler.ProcessRequest(HttpContext httpContext) at System.Web.HttpApplication.CallHandlerExecutionStep.System.Web.HttpApplication.IExecutionStep.Execute() at System.Web.HttpApplication.ExecuteStep(IExecutionStep step, Boolean& completedSynchronously)

This error message certainly raised eyebrows, as of course, this action does exist.

This is the current definition of the display article action:

[OutputCache(CacheProfile = "Short")]
[AcceptVerbs(HttpVerbs.Get)]
public ActionResult Display(string id, bool? posted)
{
}

As soon as we looked at the code, we realised what had happened. By default both Sitemap Creator and WebCopy make HEAD requests to obtain the headers for a given URL, such as the content type. They use these headers to determine if they should go ahead and download the entire file - Sitemap Creator won't download anything that isn't text/html for example.

And this is the problem - in the last update to cyotek.com, we changed a few site settings to stop the number of error emails occurring due to spammer activity. For some reason the AcceptVerbs attribute was applied to the Display action method at this point. And as it is only set to accept GET, it means our HEAD calls automatically fail.

One changing the attribute, everything started working nicely again.

[AcceptVerbs(HttpVerbs.Get | HttpVerbs.Head)]

For once, a nice and simple mystery to solve, and a nice little tip which will hopefully help anyone else who has a similar issue.

Creating a WYSIWYG font ComboBox using C#

$
0
0

This article shows how to use the built in ownerdraw functionality of a standard Windows Forms ComboBox control to display a WYSIWYG font list.

The FontComboBox control in a sample application

Setting up the control

To start, we'll create a new class, and inherit this from the ComboBox control.

We are going to use variable ownerdraw for this sample, as it gives us a little more flexibility without having to mess around with the ItemHeight property. We'll add a constructor, and set the ownerdraw mode here. Also, we'll add a new version of the DrawMode property, which we'll both hide and disable the value persistence. We always want the font list to be sorted, so for now we'll do the same with the Sorted property.

    public FontComboBox()
    {
      this.DrawMode = DrawMode.OwnerDrawVariable;
      this.Sorted = true;
    }

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

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

Caching Font objects

In order to avoid continuously creating and destroying font objects, we'll create a internal cache of fonts. When it's time to draw the control, the cache will be queried - if the requested font exists, it will be returned, otherwise the font will be created and added to the cache. This will be done via the GetFont method below.

    protected virtual Font GetFont(string fontFamilyName)
    {
      lock (_fontCache)
      {
        if (!_fontCache.ContainsKey(fontFamilyName))
        {
          Font font;

          font = this.GetFont(fontFamilyName, FontStyle.Regular);
          if (font == null)
            font = this.GetFont(fontFamilyName, FontStyle.Bold);
          if (font == null)
            font = this.GetFont(fontFamilyName, FontStyle.Italic);
          if (font == null)
            font = this.GetFont(fontFamilyName, FontStyle.Bold | FontStyle.Italic);

          _fontCache.Add(fontFamilyName, font);
        }
      }

      return _fontCache[fontFamilyName];
    }

    protected virtual Font GetFont(string fontFamilyName, FontStyle fontStyle)
    {
      Font font;

      try
      {
        font = new Font(fontFamilyName, this.PreviewFontSize, fontStyle);
      }
      catch
      {
        font = null;
      }

      return font;
    }

Note: Whilst testing the control, I discovered that some of the fonts installed on the development system only had bold or italic styles. The original version of this method, which always attempts to get the normal style would cause a crash.

Due to this, I changed the method to try and access the normal style, and if that failed, to try the other styles. Perhaps there is a better way of doing this, but I leave that as an exercise for the future.

As we don't want the font size of the dropdown list to necessarily match that of the display/edit portion, we'll add a new property named PreviewFontSize.

    public event EventHandler PreviewFontSizeChanged;

    [Category("Appearance"), DefaultValue(12)]
    public int PreviewFontSize
    {
      get { return _previewFontSize; }
      set
      {
        _previewFontSize = value;

        this.OnPreviewFontSizeChanged(EventArgs.Empty);
      }
    }

    protected virtual void OnPreviewFontSizeChanged(EventArgs e)
    {
      if (PreviewFontSizeChanged != null)
        PreviewFontSizeChanged(this, e);

      this.CalculateLayout();
    }

When certain actions occur, such as this property changing, we want to calculate the height of items in the dropdown list.

    private void CalculateLayout()
    {
      this.ClearFontCache();

      using (Font font = new Font(this.Font.FontFamily, (float)this.PreviewFontSize))
      {
        Size textSize;

        textSize = TextRenderer.MeasureText("yY", font);
        _itemHeight = textSize.Height + 2;
      }
    }

Loading the list of font families

In order to avoid slowing the control down without reason, we'll delay loading the list of font families until there is a reason - either when the control's text has changed, or when the control gets focus.

This will be done by creating a LoadFontFamilies method which will be called by overriding OnGotFocus and OnTextChanged.

    public virtual void LoadFontFamilies()
    {
      if (this.Items.Count == 0)
      {
        Cursor.Current = Cursors.WaitCursor;

        foreach (FontFamily fontFamily in FontFamily.Families)
          this.Items.Add(fontFamily.Name);

        Cursor.Current = Cursors.Default;
      }
    }

    protected override void OnGotFocus(EventArgs e)
    {
      this.LoadFontFamilies();

      base.OnGotFocus(e);
    }

    protected override void OnTextChanged(EventArgs e)
    {
      base.OnTextChanged(e);

      if (this.Items.Count == 0)
      {
        int selectedIndex;

        this.LoadFontFamilies();

        selectedIndex = this.FindStringExact(this.Text);
        if (selectedIndex != -1)
          this.SelectedIndex = selectedIndex;
      }
    }

Drawing the items

Drawing an overdraw ComboBox is done by overriding the OnDrawItem method. However, as we have told the control we are doing variable sized ownerdraw, we also need to override OnMeasureItem. This method allows us to define the size for each item, or in the case of this control to set the height of each item to match the pixel height calculated for the value of the PreviewFontSize property.

    protected override void OnMeasureItem(MeasureItemEventArgs e)
    {
      base.OnMeasureItem(e);

      if (e.Index > -1 && e.Index < this.Items.Count)
      {
        e.ItemHeight = _itemHeight;
      }
    }

 protected override void OnDrawItem(DrawItemEventArgs e)
    {
      base.OnDrawItem(e);

      if (e.Index > -1 && e.Index < this.Items.Count)
      {
        e.DrawBackground();

        if ((e.State & DrawItemState.Focus) == DrawItemState.Focus)
          e.DrawFocusRectangle();

        using (SolidBrush textBrush = new SolidBrush(e.ForeColor))
        {
          string fontFamilyName;

          fontFamilyName = this.Items[e.Index].ToString();
          e.Graphics.DrawString(fontFamilyName, this.GetFont(fontFamilyName), textBrush, e.Bounds, _stringFormat);
        }
      }
    }

The actual drawing is very simple - we use the built in drawing for the background and focus rectangle, and then use the Graphics object to draw the text using the GetFont method explained above.

You might notice that the above code is referencing a previously defined StringFormat object. This is created using the below method.

    protected virtual void CreateStringFormat()
    {
      if (_stringFormat != null)
        _stringFormat.Dispose();

      _stringFormat = new StringFormat(StringFormatFlags.NoWrap);
      _stringFormat.Trimming = StringTrimming.EllipsisCharacter;
      _stringFormat.HotkeyPrefix = HotkeyPrefix.None;
      _stringFormat.Alignment = StringAlignment.Near;
      _stringFormat.LineAlignment = StringAlignment.Center;

      if (this.IsUsingRTL(this))
        _stringFormat.FormatFlags |= StringFormatFlags.DirectionRightToLeft;
    }

    private bool IsUsingRTL(Control control)
    {
      bool result;

      if (control.RightToLeft == RightToLeft.Yes)
        result = true;
      else if (control.RightToLeft == RightToLeft.Inherit && control.Parent != null)
        result = IsUsingRTL(control.Parent);
      else
        result = false;

      return result;
    }

Cleaning up

As we are creating a large number of objects, we need to clean these up in the controls Dispose method.

    protected override void Dispose(bool disposing)
    {
      this.ClearFontCache();

      if (_stringFormat != null)
        _stringFormat.Dispose();

      base.Dispose(disposing);
    }

    protected virtual void ClearFontCache()
    {
      if (_fontCache != null)
      {
        foreach (string key in _fontCache.Keys)
          _fontCache[key].Dispose();
        _fontCache.Clear();
      }
    }

Suggestions for improvement

The control as it stands is a basic example, and depending on your application's needs, it could be further expanded. For example:

  • Currently each instance of the control will use its own font cache. By making the cache and access methods static, a single cache could be used by all instances
  • When you select a font in Word, this is added to a kind of "recently used" list at the top of Word's own font picker. The same sort of functionality could be quite easily added to this control.
  • Currently the font text is displayed on a single line. If the control isn't wide enough, the text is trimmed and therefore it may not be always possible to tell the full name of a font. Either tooltip support or drawing across multiple lines could help with this, or by resizing the dropdown component to be the minimum width required to display all font names without trimming.

Full source

The full source of the class is below.

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Drawing;
using System.Drawing.Text;
using System.Windows.Forms;

namespace Cyotek.Windows.Forms
{
  public class FontComboBox : ComboBox
  {
  #region  Private Member Declarations  

    private Dictionary<string, Font> _fontCache;
    private int _itemHeight;
    private int _previewFontSize;
    private StringFormat _stringFormat;

  #endregion  Private Member Declarations  

  #region  Public Constructors  

    public FontComboBox()
    {
      _fontCache = new Dictionary<string, Font>();

      this.DrawMode = DrawMode.OwnerDrawVariable;
      this.Sorted = true;
      this.PreviewFontSize = 12;

      this.CalculateLayout();
      this.CreateStringFormat();
    }

  #endregion  Public Constructors  

  #region  Events  

    public event EventHandler PreviewFontSizeChanged;

  #endregion  Events  

  #region  Protected Overridden Methods  

    protected override void Dispose(bool disposing)
    {
      this.ClearFontCache();

      if (_stringFormat != null)
        _stringFormat.Dispose();

      base.Dispose(disposing);
    }

    protected override void OnDrawItem(DrawItemEventArgs e)
    {
      base.OnDrawItem(e);

      if (e.Index > -1 && e.Index < this.Items.Count)
      {
        e.DrawBackground();

        if ((e.State & DrawItemState.Focus) == DrawItemState.Focus)
          e.DrawFocusRectangle();

        using (SolidBrush textBrush = new SolidBrush(e.ForeColor))
        {
          string fontFamilyName;

          fontFamilyName = this.Items[e.Index].ToString();
          e.Graphics.DrawString(fontFamilyName, this.GetFont(fontFamilyName), textBrush, e.Bounds, _stringFormat);
        }
      }
    }

    protected override void OnFontChanged(EventArgs e)
    {
      base.OnFontChanged(e);

      this.CalculateLayout();
    }

    protected override void OnGotFocus(EventArgs e)
    {
      this.LoadFontFamilies();

      base.OnGotFocus(e);
    }

    protected override void OnMeasureItem(MeasureItemEventArgs e)
    {
      base.OnMeasureItem(e);

      if (e.Index > -1 && e.Index < this.Items.Count)
      {
        e.ItemHeight = _itemHeight;
      }
    }

    protected override void OnRightToLeftChanged(EventArgs e)
    {
      base.OnRightToLeftChanged(e);

      this.CreateStringFormat();
    }

    protected override void OnTextChanged(EventArgs e)
    {
      base.OnTextChanged(e);

      if (this.Items.Count == 0)
      {
        int selectedIndex;

        this.LoadFontFamilies();

        selectedIndex = this.FindStringExact(this.Text);
        if (selectedIndex != -1)
          this.SelectedIndex = selectedIndex;
      }
    }

  #endregion  Protected Overridden Methods  

  #region  Public Methods  

    public virtual void LoadFontFamilies()
    {
      if (this.Items.Count == 0)
      {
        Cursor.Current = Cursors.WaitCursor;

        foreach (FontFamily fontFamily in FontFamily.Families)
          this.Items.Add(fontFamily.Name);

        Cursor.Current = Cursors.Default;
      }
    }

  #endregion  Public Methods  

  #region  Public Properties  

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

    [Category("Appearance"), DefaultValue(12)]
    public int PreviewFontSize
    {
      get { return _previewFontSize; }
      set
      {
        _previewFontSize = value;

        this.OnPreviewFontSizeChanged(EventArgs.Empty);
      }
    }

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

  #endregion  Public Properties  

  #region  Private Methods  

    private void CalculateLayout()
    {
      this.ClearFontCache();

      using (Font font = new Font(this.Font.FontFamily, (float)this.PreviewFontSize))
      {
        Size textSize;

        textSize = TextRenderer.MeasureText("yY", font);
        _itemHeight = textSize.Height + 2;
      }
    }

    private bool IsUsingRTL(Control control)
    {
      bool result;

      if (control.RightToLeft == RightToLeft.Yes)
        result = true;
      else if (control.RightToLeft == RightToLeft.Inherit && control.Parent != null)
        result = IsUsingRTL(control.Parent);
      else
        result = false;

      return result;
    }

  #endregion  Private Methods  

  #region  Protected Methods  

    protected virtual void ClearFontCache()
    {
      if (_fontCache != null)
      {
        foreach (string key in _fontCache.Keys)
          _fontCache[key].Dispose();
        _fontCache.Clear();
      }
    }

    protected virtual void CreateStringFormat()
    {
      if (_stringFormat != null)
        _stringFormat.Dispose();

      _stringFormat = new StringFormat(StringFormatFlags.NoWrap);
      _stringFormat.Trimming = StringTrimming.EllipsisCharacter;
      _stringFormat.HotkeyPrefix = HotkeyPrefix.None;
      _stringFormat.Alignment = StringAlignment.Near;
      _stringFormat.LineAlignment = StringAlignment.Center;

      if (this.IsUsingRTL(this))
        _stringFormat.FormatFlags |= StringFormatFlags.DirectionRightToLeft;
    }

    protected virtual Font GetFont(string fontFamilyName)
    {
      lock (_fontCache)
      {
        if (!_fontCache.ContainsKey(fontFamilyName))
        {
          Font font;

          font = this.GetFont(fontFamilyName, FontStyle.Regular);
          if (font == null)
            font = this.GetFont(fontFamilyName, FontStyle.Bold);
          if (font == null)
            font = this.GetFont(fontFamilyName, FontStyle.Italic);
          if (font == null)
            font = this.GetFont(fontFamilyName, FontStyle.Bold | FontStyle.Italic);
          if (font == null)
            font = (Font)this.Font.Clone();

          _fontCache.Add(fontFamilyName, font);
        }
      }

      return _fontCache[fontFamilyName];
    }

    protected virtual Font GetFont(string fontFamilyName, FontStyle fontStyle)
    {
      Font font;

      try
      {
        font = new Font(fontFamilyName, this.PreviewFontSize, fontStyle);
      }
      catch
      {
        font = null;
      }

      return font;
    }

    protected virtual void OnPreviewFontSizeChanged(EventArgs e)
    {
      if (PreviewFontSizeChanged != null)
        PreviewFontSizeChanged(this, e);

      this.CalculateLayout();
    }

  #endregion  Protected Methods  
  }
}

Enabling shell styles for the ListView and TreeView controls in C#

$
0
0

For those who remember the Common Controls OCX's featured in Visual Basic 5 and 6, there was one peculiarity of these. In Visual Basic 5, the Common Controls were linked directly to their shell counterparts. As the shell was updated, so did the look of any VB app using these. However, for Visual Basic 6, this behaviour was changed and they didn't use the shell for drawing.

Curiously enough, history repeats itself in a limited way with Visual Studio .NET. If you use the ListView or TreeView controls on Windows Vista or higher, you'll find they are somewhat drawn according to the "classic" Windows style - no gradients on selection highlights, column separators (ListView) or alternate +/- glyphs (TreeView).

Examples of the default TreeView and ListView controls in Windows 7

Fortunately however, it is quite simple to enable this with a single call to the SetWindowTheme API when creating the control.

    [DllImport("uxtheme.dll", CharSet = CharSet.Unicode)]
    public extern static int SetWindowTheme(IntPtr hWnd, string pszSubAppName, string pszSubIdList);

In the sample application (available for download from the link below), we create two new ListView and TreeView classes which inherit from their System.Windows.Forms counterparts.

In each class, override the OnHandleCreated method, and check to see what OS is being run - if you try to call SetWindowTheme on an unsupported OS, you'll get a crash. In this case, I'm checking for Windows Vista or higher.

If the version is fine, call SetWindowTheme with the handle of the control, and the name of the shell style - explorer in this case.

It's as simple as that - now when you run the application, the controls will be drawn using whatever shell styles are in use.

using System;

namespace ShellControlsExample
{
  class TreeView : System.Windows.Forms.TreeView
  {
    protected override void OnHandleCreated(EventArgs e)
    {
      base.OnHandleCreated(e);

      if (!this.DesignMode && Environment.OSVersion.Platform == PlatformID.Win32NT && Environment.OSVersion.Version.Major >= 6)
        NativeMethods.SetWindowTheme(this.Handle, "explorer", null);
    }
  }
}

For the TreeView control, I'd also recommend setting the ShowLines property to false as it will look odd otherwise.

Examples of the TreeView and ListView controls in Windows 7 after using the SetWindowTheme API

Downloads

Migrating from Visual SourceSafe to Subversion

$
0
0

For years now, we've used Microsoft Visual SourceSafe (VSS) for our source code control, but given that Microsoft dropped support for it some time ago in favour of Team Foundation Server, we've decided to switch VSS with an open source system. This article describes our experiences with a test migration.

As I tend to prefer the client/server model I decided to trial Subversion (SVN) over other systems such as Git or Mercury. I also want to be able to import the VSS databases containing our current code, and the one with the legacy VB6 components we used to offer.

Installing SVN

As SVN is a client/server system, you will need to install both the SVN server and a client to connect to it. Or multiple clients depending on what integration options you need. For this migration test, I'll install everything locally, but for real use you'll want to install the server software and repositories on a server. You'll still need to install the client locally however.

As SVN is open source, there are multiple clients and servers out there - some free, some not so free.

Choosing a SVN Server

I don't have a lot of experience with SVN - I've used TortoiseSVN and AnkhSVN for client side. For the server, I choose to go with VisualSVN Server as it promised to be easy enough to install - and it is free (or at least the standard version is). And surprisingly enough, it was - a few simple clicks and the server was installed.

When you install you'll be asked if you want to use HTTP or HTTPS - HTTPS will be more secure, but you'll need to verify (or replace) the default certificate or the migration tool will fail.

As our VSS user accounts mirror Windows users, I choose to enable the Windows authentication option. As this is the free version of VisualSVN, I have to use Basic (meaning you'll be prompted to enter credentials), if you want Integrated (where your Windows credentials are used automatically) you need to purchase the Enterprise license.

Creating a repository

Next, I fired up VisualSVN Server Management console and created a repository ready for the migration. To do this, right click the Repositories node and choose Create New Repository from the context menu. Enter the name of your repository, and check the option if you want the default SVN structure created.

Creating a new SVN repository

In the next section, you'll need the URL of the repository that you just created. If you didn't copy this from the Create dialog, right click the node for your repository, and choose Copy URL to Clipboard from the context menu.

Copying the URL of an existing SVN repository

Verifying the certificate

If you configure VisualSVN Server to use HTTPS, it seems to automatically create a self-certified certificate. As it is not issued by a trusted authority however, you'll run into problems trying to actually use SVN. For example, running an SVN command will issue the following error

Error validating certificate for '<servername>': The certificate is not issued by a trusted authority

And running the migration tool will offer this:

OPTIONS of '<repository>': Server certificate verification failed: issuer is not trusted (https://<servername>)

To work around this, we need to get SVN to permanently accept the certificate, which can be done from the command line.

  • Open a new command session, and browse to the bin subfolder of your VisualSVN Server installation
  • Enter the command svn ls <repository> where <repository> is the URL of your repository.
  • When the message is displayed informing you about the certificate, press p followed by enter to accept it.

This will allow you to use the command line tools, and will also prevent the migration tool discussed below from crashing when you try and use it.

Enabling a certificate for use with SVN

Gaining access to the Repository folder

When VisualSVN was installed and it created a folder for storing repositories, it doesn't actually give access to that folder for the current user. This is something else that will cause the migration tool to fail. Using Windows 7, when trying to access the folder, you are both told you don't have permission and have a "one click" option to give you permission. For other operating systems, you will probably have to edit the folder permissions manually to grant yourself access.

Trying to access the Repository folder gives a permission error

Before performing the migration, ensure that you have access to the repositories folder.

Choosing a SVN Client

As mentioned above, the only SVN client I've used in the past was TortoiseSVN. I've no doubt there's other clients out there (while VisualSVN does have a client, you do need to pay for it), but I decided to stick with this. As with the VisualSVN Server, installation is a breeze. Irritatingly, you are not able to change the installation directory.

Choosing a Visual Studio SCC Provider

TortoiseSVN is all well and good for managing from Windows Explorer, but that isn't quite good enough. I need support in the Visual Studio 2010 IDE, and for this I've gone with AnkhSVN. As with the other two products, installation of AnkhSVN was quick and painless.

Preparing SourceSafe for migration

  • Ensure all files are checked in
  • If you are using SourceSafe 6, install the final service pack for Visual Studio 6
  • If you are using SourceSafe 2005 (version 8), you may wish to install this patch from Microsoft.
  • Run the analyze.exe tool to ensure your VSS database contains no errors

Migrating a SourceSafe Database

As neither SVN nor VSS have appropriate import/export functionality, we need to turn to the community for help. Fortunately, there's a tool you can download from PowerAdmin. In addition to the original tool there's also user supplied contributions - I went with Update 5, which is the latest at the time of writing and is supplied as C# source developed using Visual Studio 2008. While other versions using C++ are available, the remainder of this article discusses the C# version.

Setting up the migration

Before you run the migration, you need to configure a number of parameters. Open up the tool downloaded from the link above in Visual Studio - if you're using Visual Studio 2010 you'll be asked to upgrade the project.

The first thing you'll need to do if you are running a 64bit OS is to configure the project to be compiled as a 32bit application otherwise the first thing you're going to get is a nasty COM error.

The migration tool must be run as a 32bit process in order for SourceSafe interop to work

Next, open up app.config - there's lots of values in here to change.

  • VSSDIR - name of the folder where your VSS database is located (the folder which contains srcsafe.ini)
  • VSSPROJ - Name of the project to import. According to the tools documentation, you shouldn't specify the SourceSafe root, but instead the initial children. (Meaning if you have multiple projects at the root level you'll have to run this tool multiple times.)
  • VSSUSER - VSS login user name
  • VSSPASSWORD - VSS login password
  • SVNUSER - SVN login name
  • SVNPASSWORD - SVN login password
  • SVNURL - URL of your repository
  • SVNPROJ - name of the SVN project to create
  • SVNREVPROPSPATH - the local folder where your repository will be stored
  • WORKDIR - temporary directory where local files will be extracted. Make sure this folder actually exists before running the tool! In addition, the tool will create a folder named _migrate - make sure this folder doesn't exist or the tool will crash when trying to create folders if they exist.

Creating a new SVN repository

With this information specified you're good to go - just run the solution. And go make a cup of tea, you'll be waiting a while, depending on the size of the database. I've tested with two databases, one 40MB and one 4GB - the former took an hour, the later 26 hours for a partial import.

Notification after a successful migration

Sadly, whilst the PowerAdmin migration tool supports comments, it doesn't seem to support labels, so if your database uses these they won't be present.

Verifying the migration

On returning to the VisualSVN Server console, it wasn't showing the new projects - pressing F5 to refresh the list solved that, and I could see all the projects that had been imported. So far so good. Now time to check a file.

And this is where VisualSVN fell flat - it seems that you can only view the latest version of a file. So with that said, I opened Windows Explorer, right clicked the file list and chose TortoiseSVN | Repro-browser from the context menu. I then search for a file which I knew had been checked in numerous times, and was able to view it's full history... even the dates were correct. Comparing a few random revisions of the file looked fine too.

Viewing the SVN log for a file

I could see the initial comments from when the file was first added, but as I suspected no label comments were present.

The next thing to do would be to compare the full repository against the SourceSafe working copy.

To do this, I returned to the root project in the Repository Browser, right clicked it and choose Checkout. I entered a folder name (make sure this is something which doesn't already exist) and then checked out the entire trunk. After that was all checked out I compared the two folders using WinMerge.

The comparison looked good - source code was identical, except for the Visual Studio project files - the migration tool automatically removes the VSS bindings from them which is a nice touch.

Once you open your solution, you need to tell Visual Studio to use AnkhSVN. To do this, open the Visual Studio Options dialog and select the Source Control | Plug-in Selection section. Simply select AnkhSVN from the dropdown list and you can then use SVN from within the Visual Studio IDE, in addition to Windows Explorer via TortoiseSVN.

Configuring Visual Studio to use AnkhSVN

Things that can go wrong

Although the migration of our .NET code was successful, when testing with the database containing the old VB6 components we used to offer, the migration would crash for two different reasons:

The first was an occasional (and somewhat random) Access Denied when trying to commit files. Retrying the commit always worked, but it was at this point that I found the AUTORETRY setting isn't actually used. I manually updated the migration tool to retry commits in this case.

The second one would be the following error when trying to get a file from VSS.

SourceSafe was unable to finish writing a file. Check your available disk space, and ask the administrator to analyse your SourceSafe database.

This patch apparently has a fix for this but I haven't tried it yet.

Once I have resolved these errors I'll post an updated version of the migration tool. With workarounds in place, the migration still completed successfully so I'm cautiously optimistic for updating the tool and doing a true migration and switch.

Conclusion

It's still a little early for me to say whether we'll stick with SVN or try something else. While I never did like SourceSafe's habit of cluttering directories with .scc files, I am even less enamoured of the .svn directories and the additional disk space they take. But I'm sure the fact that it's far less likely that a repository will be corrupt!

Another minor annoyance; we currently have Visual Studio set to check in each time the solution is closed. This functionality doesn't exist in AnkhSVN and so a manual commit will be required - there are arguments both for and against this type of functionality, I happen to prefer having the code constantly updated the central store, regardless of whether or not it actually builds.

The main problem is currently the loss of labels - tracking down the source code for a particular product build currently looks like a fairly large problem. It may be that the migration tool can be modified to support this, which I'll be looking into over the coming days.

Importing a SourceSafe database into Subversion

$
0
0

In my previous article on migrating a VSS database into SVN, I hadn't tried importing a VSS database into an existing repository, which is something I found the original code couldn't handle.

I rewrote the original tool to have a GUI front end in addition to a console version, updated to allow importing into existing repositories, and fixed a crash which would occur when trying to import a file which couldn't be retrieved from SourceSafe. The updated tool worked enough for my purposes, but hasn't been extensively tested. However, as my three VSS databases are now in SVN it's unlikely I'll be making further updates to the code.

The source code for the tool has therefore been uploaded as Open Source on CodePlex.com in the hope that someone else finds it useful. Some limited documentation is also available on the CodePlex site, but if anyone has questions, please leave a comment or contact us.

Downloads


CSS Syntax Highlighting in the DigitalRune Text Editor Control

$
0
0

For projects where I need some form of syntax highlighting, I tend to use the open source DigitalRune Text Editor Control which is a modified version of the text editor used in SharpDevelop. While it has a number of syntax definitions built in, the one it didn't have was for CSS formatting.

After doing a quick search on the internet and finding pretty much nothing, I created my own. This article describes that process, along with how to embed the definition directly in a custom version of the control, or loading it into the vendor supplied control.

A sample application demonstrating CSS syntax highlighting

Creating the rule set

Each definition is an XML document which contains various sections describing how to syntax highlight a document. An XSD schema is available, named Mode.xsd and located in the /Resources directory in the control's source code.

Here's an example of an (almost) empty definition - I've filled in the definition name and the list of file extensions it will support:

<?xml version="1.0" encoding="utf-8"?><SyntaxDefinition name="CSS" extensions="*.css"><RuleSets></RuleSets></SyntaxDefinition>

The RuleSets element contains one of more RuleSet elements which in turn describe formatting. I'm not sure how the control decides to process these, but in my example I started with an unnamed ruleset which references a named ruleset, and in turn that references another - seems to work fine.

There are two key constructs we'll be using for highlighting - first is span highlighting, where an block of text which starts and ends with given symbols is highlighted. The second is keywords, where distinct words are highlighted. From having a quick look through the source code to figure out problems, there appears to be one or two other constructs available, but I'll ignore these for now.

First, I need to add a rule for comments, which should be quite straight forward - look for a /* and end with /*:

<RuleSet ignorecase="false"><Span name="Comment" bold="false" italic="false" color="Green" stopateol="false"><Begin>/*</Begin><End>*/</End></Span></RuleSet>

The Span tag creates a span highlighting construct. The Begin and End tags describe the phrase that marks the beginning and end of the text to match. The stopateol attribute determines if the line breaks should stop at the end of a line. The formatting properties should be evident!

Next, I added another span rule to process the highlighting of the actual CSS rules - so anything between { and }.

<Span name="CssClass" rule="CssClass" bold="false" italic="false" color="Black" stopateol="false"><Begin>{</Begin><End>}</End></Span>

Note this time the rule attribute - this is pointing to a new ruleset (more on that below). Without this attribute, I found that I was unable to style keywords and values inside the CSS rule, as the span above always took precedence. The new ruleset looks similar to this, although in this example I have stripped out most of the CSS property names. (The list of which came from w3schools)

<RuleSet name="CssClass" ignorecase="true"><Span name="Value" rule="ValueRules" bold="false" italic="false" color="Blue" stopateol="false"><Begin color="Black">:</Begin><End color="Black">;</End></Span><KeyWords name="CSSLevel1PropertyNames" bold="false" italic="false" color="Red"><Key word="background" /><Key word="background-attachment" />
        (snip)</KeyWords><KeyWords name="CSSLevel2PropertyNames" bold="false" italic="false" color="Red"><Key word="border-collapse" /><Key word="border-spacing" />
        (snip)</KeyWords><KeyWords name="CSSLevel3PropertyNames" bold="false" italic="false" color="Red"><Key word="@font-face" /><Key word="@keyframes" />
        (snip)</KeyWords></RuleSet>

First is a new span to highlight attribute values (found between the : and ; characters in blue, and then 3 sets of a new construct - KeyWords. This basically matches a given word and formats it appropriately. In this example, I have split each of the 3 major CSS versions into separate sections, on the off chance you want to reconfigure the file to only support a subset, for example CSS1 and CSS2. Also note that I haven't included any vendor prefixes.

One thing to note, in the Value span above, the begin and end tags have color attributes. This overrides the overall span color (blue) and colors those individual colors with the override (black). Again, from checking the scheme it looks like this can be done for most elements, and supports the color, bold and italic attributes, plus a bgcolor attribute that I haven't used yet.

The span in the above ruleset references a final ruleset, as follows:

<RuleSet name="ValueRules" ignorecase="false"><Span name="Comment" bold="false" italic="false" color="Green" stopateol="false"><Begin>/*</Begin><End>*/</End></Span><<pan name="String" bold="false" italic="false" color="BlueViolet" stopateol="true"><Begin>"</Begin><End>"</End></Span><Span name="Char" bold="false" italic="false" color="BlueViolet" stopateol="true"><Begin>'</Begin><End>'</End></Span><KeyWords name="Flags" bold="true" italic="false" color="BlueViolet"><Key word="!important" /></KeyWords></RuleSet>

This ruleset has 3 spans, and one keyword. I had to duplicate the comment span from the first ruleset, I couldn't comment highlighting to work inside { } blocks otherwise - probably some subtlety of the definition format that I'm missing. This is followed by two spans which highlight strings (depending on whether single or double quoted). Finally, we have a keyword rule for formatting !important. (Of course, ideally you wouldn't be using this keyword at all, but you never know!)

Put togehter, this definition nicely highlights CSS. Except for one thing - everything outside a comment or style block is black. And I want it to be something else! Initially I tried just setting the ForeColor property of the control itself, but this was blatantly ignored when it drew itself. Fortunately a scan of the schema gave the answer - you can add an Environment tag and set up a large bunch of colors. Or one, in this case.

<Environment><Default color="Maroon" bgcolor="White" /></Environment>

Now save the file somewhere with the .xshd extension - in keeping with the convention of the existing definitions, I named it CSS-Mode.xshd.

Loading the definition into the Text Editor control

This is where I was a little bit stumped - as I didn't have a clue how to get the definition in. Fortunately, DigitalRune's technical support were able to help.

If you are using a custom version of the source code, you can add the definition directly into the source and have it available with the compiled assembly. However, if you are using the vendor supplied assembly, you'll need to include the definition with your application in order to load it in.

Compiling the definition into the assembly

This is quite straight forward, and easily recommended if you have a custom version.

  1. Copy the definition file into the Resources folder of the control's project
  2. Set the Build Action to be Embedded Resource
  3. Open SyntaxModes.xml located in the same folder and add a mode tag which points to your definition, for example
    <Mode file="CSS-Mode.xshd" name="CSS" extensions=".css"/>
    While I haven't checked to see if it is enforced, common sense would suggest you ensure the name and extensions attributes match in both the syntax definition and the ruleset definition.
  4. Compile the solution.

With that done, your definition is now available for use!

Loading the definitions externally

You don't need to compile the definitions into the control assembly, but can load them externally. To do this, you need to have the definition file and the syntax mode file available for loading.

  1. Add a new folder to your project and copy into this folder your .xshd file and set the Copy to Output Directory property to Copy always.
  2. Create a file named SyntaxMode.xml in the folder, and paste in the definition below. You'll also need to set the copy to output directory attribute.
    <?xml version="1.0" encoding="utf-8"?><SyntaxModes version="1.0"><Mode file="CSS-Mode.xshd" name="CSS" extensions=".css" /></SyntaxModes>
  3. The following line of code will load the definition file into the text editor control:
    HighlightingManager.Manager.AddSyntaxModeFileProvider(new FileSyntaxModeProvider(definitionsFolder));

Setting up the Text Editor Control

To instruct instances of the Text Editor control to use CSS syntax highlighting, add the following line of code to your application (replacing CSS with the name of your definition if you called it something different):

textEditorControl.Document.HighlightingStrategy = HighlightingManager.Manager.FindHighlighter("CSS");

Syntax highlighting isn't appearing, what went wrong?

Rather frustratingly, the control doesn't raise an error if a definition file is invalid, it just silently ignores it and uses a default highlighting scheme. Use the source code for the control so you can catch the exceptions being raised by the HighlightingDefinitionParser class in order to determine any problems. Remember the definition you create is implicitly linked to the schema and so must conform to it.

SharpDevelop?

As the DigitalRune control is derived from the original SharpDevelop editing component, I believe this article and sample code will work in exactly the same way for the SharpDevelop control. However, I don't have this installed and so this remains untested - let me know if it works for you!

Sample application

The download available below includes the CSS definition file and a sample application which will load in the definition files. Note that no binaries are included in the archive, you'll need to add a reference to a copy of the DigitalRune Text Editor control installed on your own system.

Downloads

Detecting if a given font style exists in C#

$
0
0

In a previous article, Creating a WYSIWYG font ComboBox using C#, there is a hacky bit of code which uses a try catch block to handle processing when a given font style doesn't exist. This article describes a better way of handling this requirement without relying on the exception handler.

Originally we used the following code to determine if a font style exists: (Some of the additional code has been removed for clarity)

   protected virtual Font GetFont(string fontFamilyName)
    {
      Font font;

      font = this.GetFont(fontFamilyName, FontStyle.Regular);
      if (font == null)
        font = this.GetFont(fontFamilyName, FontStyle.Bold);
      if (font == null)
        font = this.GetFont(fontFamilyName, FontStyle.Italic);
      if (font == null)
        font = this.GetFont(fontFamilyName, FontStyle.Bold | FontStyle.Italic);

      return font;
    }

    protected virtual Font GetFont(string fontFamilyName, FontStyle fontStyle)
    {
      Font font;

      try
      {
        font = new Font(fontFamilyName, this.PreviewFontSize, fontStyle);
      }
      catch
      {
        font = null;
      }

      return font;
    }

This code essentially "tests" each style by attempting to create a font instance of a given style. If the style doesn't exist, an exception is thrown and the code moves onto the next style.

A better way is to use the IsStyleAvailable function of the FontFamily object. You simply create an instance of this object with the name of the font you wish to query, then call the method with the style to test. Note that the constructor for FontFamily will throw an exception if the font you try to create doesn't exist.

Switching the GetFont method above to use IsStyleAvailable ends up looking like this:

    protected virtual Font GetFont(string fontFamilyName)
    {
      Font font;

      using (FontFamily family = new FontFamily(fontFamilyName))
      {
        if (family.IsStyleAvailable(FontStyle.Regular))
          font = this.GetFont(fontFamilyName, FontStyle.Regular);
        else if (family.IsStyleAvailable(FontStyle.Bold))
          font = this.GetFont(fontFamilyName, FontStyle.Bold);
        else if (family.IsStyleAvailable(FontStyle.Italic))
          font = this.GetFont(fontFamilyName, FontStyle.Italic);
        else if (family.IsStyleAvailable(FontStyle.Bold | FontStyle.Italic))
          font = this.GetFont(fontFamilyName, FontStyle.Bold | FontStyle.Italic);
        else
          font = null;
      }

      return font;
    }

    protected virtual Font GetFont(string fontFamilyName, FontStyle fontStyle)
    {
      return new Font(fontFamilyName, this.PreviewFontSize, fontStyle);
    }

Based on this, a simple method to check if a given font name and style exists is presented below. As the constructor for FontFamily throws an ArgumentException if the given font doesn't exist, we can trap that and return false. Any other error will be thrown, rather than being silently ignored as in our earlier solution.

    public bool DoesFontExist(string fontFamilyName, FontStyle fontStyle)
    {
      bool result;

      try
      {
        using (FontFamily family = new FontFamily(fontFamilyName))
          result = family.IsStyleAvailable(fontStyle);
      }
      catch (ArgumentException)
      {
        result = false;
      }

      return result;
    }

Convert a PDF into a series of images using C# and GhostScript

$
0
0

An application I was recently working on received PDF files from a webservice which it then needed to store in a database. I wanted the ability to display previews of these documents within the application. While there are a number of solutions for creating PDF files from C#, options for viewing a PDF within your application is much more limited, unless you purchase expensive commercial products, or use COM interop to embed Acrobat Reader into your application.

This article describes an alternate solution, in which the pages in a PDF are converted into images using GhostScript, from where you can then display them in your application.

A sample application demonstrating displaying a PDF file in the ImageBox control

In order to avoid huge walls of text, this article has been split into two parts, the first dealing with the actual conversion of a PDF, and the second demonstrates how to extend the ImageBox control to display the images.

Caveat emptor

Before we start, some quick points.

  • The method I'm about to demonstrate converts into page of the PDF into an image. This means that it is very suitable for viewing, but interactive elements such as forms, hyperlinks and even good old text selection are not available.
  • GhostScript has a number of licenses associated with it but I can't find any information of the pricing of commercial licenses.
  • The GhostScript API Integration library used by this project isn't complete and I'm not going to go into the bells and whistles of how it works in this pair of articles - once I've completed the outstanding functionality I'll create a new article for it.

Getting Started

You can download the two libraries used in this article from the links below, these are:

  • Cyotek.GhostScript - core library providing GhostScript integration support
  • Cyotek.GhostScript.PdfConversion - support library for converting a PDF document into images

Please note that the native GhostScript DLL is not included in these downloads, you will need to obtain that from the GhostScript project page.

Using the GhostScriptAPI class

As mentioned above, the core GhostScript library isn't complete yet, so I'll just give a description of the basic functionality required by the conversion library.

The GhostScriptAPI class handles all communication with GhostScript. When you create an instance of the class, it automatically calls gsapi_new_instance in the native GhostScript DLL. When the class is disposed, it will automatically release any handles and calls the native gsapi_exit and gsapi_delete_instance methods.

In order to actually call GhostScript, you call the Execute method, passing in either a string array of all the arguments to pass to GhostScript, or a typed dictionary of commands and values. The GhostScriptCommand enum contains most of the commands supported by GhostScript, which may be a preferable approach rather than trying to remember the parameter names themselves.

Defining conversion settings

The Pdf2ImageSettings class allows you to customize various properties of the output image. The following properties are available:

  • AntiAliasMode - specifies the antialiasing level between Low, Medium and High. This internally will set the dTextAlphaBits and dGraphicsAlphaBits GhostScript switches to appropriate values.
  • Dpi - dots per inch. Internally sets the r switch. This property is not used if a paper size is set.
  • GridFitMode - controls the text readability mode. Internally sets the dGridFitTT switch.
  • ImageFormat - specifies the output image format. Internally sets the sDEVICE switch.
  • PaperSize - specifies a paper size from one of the standard sizes supported by GhostScript.
  • TrimMode - specifies how the image should be sized. Your milage may vary if you try and use the paper size option. Internally sets either the dFIXEDMEDIA and sPAPERSIZE or the dUseCropBox or the dUseTrimBox switches.

Typical settings could look like this:

      Pdf2ImageSettings settings;

      settings = new Pdf2ImageSettings();
      settings.AntiAliasMode = AntiAliasMode.High;
      settings.Dpi = 300;
      settings.GridFitMode = GridFitMode.Topological;
      settings.ImageFormat = ImageFormat.Png24;
      settings.TrimMode = PdfTrimMode.CropBox;

Converting the PDF

To convert a PDF file into a series of images, use the Pdf2Image class. The following properties and methods are offered:

  • ConvertPdfPageToImage - converts a given page in the PDF into an image which is saved to disk
  • GetImage - converts a page in the PDF into an image and returns the image
  • GetImages - converts a range of pages into the PDF into images and returns an image array
  • PageCount - returns the number of pages in the source PDF
  • PdfFilename - returns or sets the filename of the PDF document to convert
  • PdfPassword - returns or sets the password of the PDF document to convert
  • Settings - returns or sets the settings object described above

A typical example to convert the first image in a PDF document:

Bitmap firstPage = new Pdf2Image("sample.pdf").GetImage();

The inner workings

Most of the code in the class is taken up with the GetConversionArguments method. This method looks at the various properties of the conversion such as output format, quality, etc, and returns the appropriate commands to pass to GhostScript:

protected virtual IDictionary<GhostScriptCommand, object> GetConversionArguments(string pdfFileName, string outputImageFileName, int pageNumber, string password, Pdf2ImageSettings settings)
    {
      IDictionary<GhostScriptCommand, object> arguments;

      arguments = new Dictionary<GhostScriptCommand, object>();

      // basic GhostScript setup
      arguments.Add(GhostScriptCommand.Silent, null);
      arguments.Add(GhostScriptCommand.Safer, null);
      arguments.Add(GhostScriptCommand.Batch, null);
      arguments.Add(GhostScriptCommand.NoPause, null);

      // specify the output
      arguments.Add(GhostScriptCommand.Device, GhostScriptAPI.GetDeviceName(settings.ImageFormat));
      arguments.Add(GhostScriptCommand.OutputFile, outputImageFileName);

      // page numbers
      arguments.Add(GhostScriptCommand.FirstPage, pageNumber);
      arguments.Add(GhostScriptCommand.LastPage, pageNumber);

      // graphics options
      arguments.Add(GhostScriptCommand.UseCIEColor, null);

      if (settings.AntiAliasMode != AntiAliasMode.None)
      {
        arguments.Add(GhostScriptCommand.TextAlphaBits, settings.AntiAliasMode);
        arguments.Add(GhostScriptCommand.GraphicsAlphaBits, settings.AntiAliasMode);
      }

      arguments.Add(GhostScriptCommand.GridToFitTT, settings.GridFitMode);

      // image size
      if (settings.TrimMode != PdfTrimMode.PaperSize)
        arguments.Add(GhostScriptCommand.Resolution, settings.Dpi.ToString());

      switch (settings.TrimMode)
      {
        case PdfTrimMode.PaperSize:
          if (settings.PaperSize != PaperSize.Default)
          {
            arguments.Add(GhostScriptCommand.FixedMedia, true);
            arguments.Add(GhostScriptCommand.PaperSize, settings.PaperSize);
          }
          break;
        case PdfTrimMode.TrimBox:
          arguments.Add(GhostScriptCommand.UseTrimBox, true);
          break;
        case PdfTrimMode.CropBox:
          arguments.Add(GhostScriptCommand.UseCropBox, true);
          break;
      }

      // pdf password
      if (!string.IsNullOrEmpty(password))
        arguments.Add(GhostScriptCommand.PDFPassword, password);

      // pdf filename
      arguments.Add(GhostScriptCommand.InputFile, pdfFileName);

      return arguments;
    }
    

As you can see from the method above, the commands are being returned as a strongly typed dictionary - the GhostScriptAPI class will convert these into the correct GhostScript commands, but the enum is much easier to work with from your code! The following is an example of the typical GhostScript commands to convert a single page in a PDF document:

-q -dSAFER -dBATCH -dNOPAUSE -sDEVICE=png16m -sOutputFile=tmp78BC.tmp -dFirstPage=1 -dLastPage=1 -dUseCIEColor -dTextAlphaBits=4 -dGraphicsAlphaBits=4 -dGridFitTT=2 -r150 -dUseCropBox=true sample.pdf

The next step is to call GhostScript and convert the PDF which is done using the ConvertPdfPageToImage method:

    public void ConvertPdfPageToImage(string outputFileName, int pageNumber)
    {
      if (pageNumber < 1 || pageNumber > this.PageCount)
        throw new ArgumentException("Page number is out of bounds", "pageNumber");

      using (GhostScriptAPI api = new GhostScriptAPI())
        api.Execute(this.GetConversionArguments(this._pdfFileName, outputFileName, pageNumber, this.PdfPassword, this.Settings));
    }

As you can see, this is a very simple call - create an instance of the GhostScriptAPI class and then pass in the list of parameters to execute. The GhostScriptAPI class takes care of everything else.

Once the file is saved to disk, you can then load it into a Bitmap or Image object for use in your application. Don't forget to delete the file when you are finished with it!

Alternatively, the GetImage method will convert the file and return the bitmap image for you, automatically deleting the temporary file. This saves you from having to worry about providing and deleting the output file, but it does mean you are responsible for disposing of the returned bitmap.

    public Bitmap GetImage(int pageNumber)
    {
      Bitmap result;
      string workFile;

      if (pageNumber < 1 || pageNumber > this.PageCount)
        throw new ArgumentException("Page number is out of bounds", "pageNumber");

      workFile = Path.GetTempFileName();

      try
      {
        this.ConvertPdfPageToImage(workFile, pageNumber);
        using (FileStream stream = new FileStream(workFile, FileMode.Open, FileAccess.Read))
          result = new Bitmap(stream);
      }
      finally
      {
        File.Delete(workFile);
      }

      return result;
    }

You could also convert a range of pages at once using the GetImages method:

public Bitmap[] GetImages(int startPage, int lastPage)
{
  List<Bitmap> results;

  if (startPage < 1 || startPage > this.PageCount)
    throw new ArgumentException("Start page number is out of bounds", "startPage");

  if (lastPage < 1 || lastPage > this.PageCount)
    throw new ArgumentException("Last page number is out of bounds", "lastPage");
  else if (lastPage < startPage)
    throw new ArgumentException("Last page cannot be less than start page", "lastPage");

  results = new List<Bitmap>();
  for (int i = startPage; i <= lastPage; i++)
    results.Add(this.GetImage(i));

  return results.ToArray();
}

In conclusion

The above methods provide a simple way of providing basic PDF viewing in your applications. In the next part of this series, we describe how to extend the ImageBox component to support conversion and navigation.

Update 10/07/2012

Downloads

Extending the ImageBox component to display the contents of a PDF file using C#

$
0
0

In this article, I'll describe how to extend the ImageBox control discussed in earlier articles to be able to display PDF files with the help of the GhostScript library and the conversion library described in the previous article.

A sample application demonstrating displaying a PDF file in the ImageBox control

Getting Started

You can download the source code used in this article from the links below, these are:

  • Cyotek.GhostScript - core library providing GhostScript integration support
  • Cyotek.GhostScript.PdfConversion - support library for converting a PDF document into images
  • PdfImageBoxSample - sample project containing an updated ImageBox control, and the extended PdfImageBox.

Please note that the native GhostScript DLL is not included in these downloads, you will need to obtain that from the GhostScript project page.

Extending the ImageBox

To start extending the ImageBox, create a new class and inherit the ImageBox control. I also decided to override some of the default properties, so I added a constructor which sets the new values.

    public PdfImageBox()
    {
      // override some of the original ImageBox defaults
      this.GridDisplayMode = ImageBoxGridDisplayMode.None;
      this.BackColor = SystemColors.AppWorkspace;
      this.ImageBorderStyle = ImageBoxBorderStyle.FixedSingleDropShadow;

      // new pdf conversion settings
      this.Settings = new Pdf2ImageSettings();
    }

To ensure correct designer support, override versions of the properties with new DefaultValue attributes were added. With this done, it's time to add the new properties that will support viewing PDF files. The new properties are:

  • PdfFileName - the filename of the PDF to view
  • PdfPassword - specifies the password of the PDF file if one is required to open it (note, I haven't actually tested that this works!)
  • Settings - uses the Pdf2ImageSettings class discussed earlier to control quality settings for the converted document.
  • PageCache - an internal dictionary which stores a Bitmap against a page number to cache pages after these have loaded.

With the exception of PageCache, each of these properties also has backing event for change notifications, and as Pdf2ImageSettings implements INotifyPropertyChanged we'll also bind an event detect when the individual setting properties are modified.

    [Category("Appearance"), DefaultValue(typeof(Pdf2ImageSettings), "")]
    public virtual Pdf2ImageSettings Settings
    {
      get { return _settings; }
      set
      {
        if (this.Settings != value)
        {
          if (_settings != null)
            _settings.PropertyChanged -= SettingsPropertyChangedHandler;

          _settings = value;
          _settings.PropertyChanged += SettingsPropertyChangedHandler;

          this.OnSettingsChanged(EventArgs.Empty);
        }
      }
    }
    
    private void SettingsPropertyChangedHandler(object sender, PropertyChangedEventArgs e)
    {
      this.OnSettingsChanged(e);
    }

    protected virtual void OnSettingsChanged(EventArgs e)
    {
      this.OpenPDF();

      if (this.SettingsChanged != null)
        this.SettingsChanged(this, e);
    }

Navigation support

Although the PdfImageBox doesn't supply a user interface for navigating to different pages, we want to make it easy for the hosting application to provide one. To support this, a new CurrentPage property will be added for allowing the active page to retrieved or set, and also a number of readonly CanMove* properties. These properties allow the host to query which navigation options are applicable in order to present the correct UI.

    [Browsable(false)]
    public virtual int PageCount
    { get { return _converter != null ? _converter.PageCount : 0; } }

    [Category("Appearance"), DefaultValue(1)]
    public int CurrentPage
    {
      get { return _currentPage; }
      set
      {
        if (this.CurrentPage != value)
        {
          if (value < 1 || value > this.PageCount)
            throw new ArgumentException("Page number is out of bounds");

          _currentPage = value;

          this.OnCurrentPageChanged(EventArgs.Empty);
        }
      }
    }

    [Browsable(false)]
    public bool CanMoveFirst
    { get { return this.PageCount != 0 && this.CurrentPage != 1; } }

    [Browsable(false)]
    public bool CanMoveLast
    { get { return this.PageCount != 0 && this.CurrentPage != this.PageCount; } }

    [Browsable(false)]
    public bool CanMoveNext
    { get { return this.PageCount != 0 && this.CurrentPage < this.PageCount; } }

    [Browsable(false)]
    public bool CanMovePrevious
    { get { return this.PageCount != 0 && this.CurrentPage > 1; } }

Again, to make it easier for the host to connect to the control, we also add some helper navigation methods.

    public void FirstPage()
    {
      this.CurrentPage = 1;
    }

    public void LastPage()
    {
      this.CurrentPage = this.PageCount;
    }

    public void NextPage()
    {
      this.CurrentPage++;
    }

    public void PreviousPage()
    {
      this.CurrentPage--;
    }

Finally, it can sometimes take a few seconds to convert a page in a PDF file. To allow the host to provide a busy notification, such as setting the wait cursor or displaying a status bar message, we'll add a pair of events which will be called before and after a page is converted.

public event EventHandler LoadingPage;

public event EventHandler LoadedPage;

Opening the PDF file

Each of the property changed handlers in turn call the OpenPDF method. This method first clears any existing image cache and then initializes the conversion class based on the current PDF file name and quality settings. If the specified file is a valid PDF, the first page is converted, cached, and displayed.

    public void OpenPDF()
    {
      this.CleanUp();

      if (!this.DesignMode)
      {
        _converter = new Pdf2Image()
        {
          PdfFileName = this.PdfFileName,
          PdfPassword = this.PdfPassword,
          Settings = this.Settings
        };

        this.Image = null;
        this.PageCache= new Dictionary<int, Bitmap>();
        _currentPage = 1;

        if (this.PageCount != 0)
        {
          _currentPage = 0;
          this.CurrentPage = 1;
        }
      }
    }

    private void CleanUp()
    {
      // release  bitmaps
      if (this.PageCache != null)
      {
        foreach (KeyValuePair<int, Bitmap> pair in this.PageCache)
          pair.Value.Dispose();
        this.PageCache = null;
      }
    }

Displaying the image

Each time the CurrentPage property is changed, it calls the SetPageImage method. This method first checks to ensure the specified page is present in the cache. If it is not, it will load the page in. Once the page is in the cache, it is then displayed in the ImageBox, and the user can then pan and zoom as with any other image.

    protected virtual void SetPageImage()
    {
      if (!this.DesignMode && this.PageCache != null)
      {
        lock (_lock)
        {
          if (!this.PageCache.ContainsKey(this.CurrentPage))
          {
            this.OnLoadingPage(EventArgs.Empty);
            this.PageCache.Add(this.CurrentPage, _converter.GetImage(this.CurrentPage));
            this.OnLoadedPage(EventArgs.Empty);
          }

          this.Image = this.PageCache[this.CurrentPage];
        }
      }
    }

Note that we operate a lock during the execution of this method, to ensure that you can't try and load the same page twice.

With this method in place, the control is complete and ready to be used as a basic PDF viewer. In order to keep the article down to a reasonable size, I've excluded some of the definitions, overloads and helper methods; these can all be found in the sample download below.

The sample project demonstrates all the features described above and provides an example setting up a user interface for navigating a PDF document.

Future changes

At the moment, the PdfImageBox control processes on page at a time and caches the results. This means that navigation through already viewed pages is fast, but displaying new pages can be less than ideal. A possible enhancement would be to make the control multithreaded, and continue to load pages on a background thread.

Another issue is that as the control is caching the converted images in memory, it may use a lot of memory in order to display large PDF files. Not quite sure on the best approach to resolve this one, either to "expire" older pages, or to keep only a fixed number in memory. Or even save each page to a temporary disk file.

Finally, I haven't put in any handling at all for if the converter fails to convert a given page... I'll add this to a future update, and hopefully get the code hosted on an SVN server for interested parties.

Downloads

Detecting if an application is running as an elevated process, and spawning a new process using elevated permissions

$
0
0

Recently I was writing some code to allow a program to register itself to start with Windows for all users. On Windows 7 with User Account Control (UAC) enabled, trying to write to the relevant registry key without having elevated permissions throws an UnauthorizedAccessException exception. If you want to make these sorts of modifications to a system, the application needs to be running as an administrator.

Checking if your application is running with elevated permissions

To check if your application is currently running with elevated permissions, you simply need to see if the current user belongs to the Administrator user group.

// Requires "using System.Security.Principal;"

public bool IsElevated
{
  get
  {
    return new WindowsPrincipal(WindowsIdentity.GetCurrent()).IsInRole(WindowsBuiltInRole.Administrator);
  }
}

public void SomeMethod()
{
  if (this.IsElevated)
  {
    // running as administrator
  }
}

Running an application as an administrator

While it might be possible to elevate your applications process via the LogonUser API, this requires user names and passwords, and isn't a trivial task. So we'll ignore this approach in favour of something much more simplistic and less likely to go wrong, not to mention not requiring admin passwords.

You're probably already aware that there are various "verbs" predefined for dealing with specific actions relating to interaction with a file, such as print and open. While these verbs are normally configured on file associations in the Windows Registry, you can also force a process to be run under the administration account by specifying the runas verb.

Note: Specifying this verb in Windows XP displays a dialog allowing a user to be selected. Unfortunately this means that it's possible for the spawned application to not have the required permissions either - remember to check that you have permission to do an action before actually attempting it!

For my scenario, the core application shouldn't need to run in elevated mode, so I decided to create a generic stub program which would accept a number of arguments for if the startup program should be registered or unregistered, and the title and location used to perform the action. Then the main application simply spawned this process in administration mode to apply the users choice.

ProcessStartInfo startInfo;

startInfo = new ProcessStartInfo();
startInfo.FileName = Path.Combine(Path.GetDirectoryName(Application.ExecutablePath), "regstart.exe"); // replace with your filename
startInfo.Arguments = string.Empty; // if you need to pass any command line arguments to your stub, enter them here
startInfo.UseShellExecute = true;
startInfo.Verb = "runas";

Process.Start(startInfo);

Note: Although I haven't included it in the example above, you may wish to handle the Win32Exception that can be thrown by the Process.Start method. If the user cancels the UAC prompt, this exception will be automatically thrown with the ERROR_CANCELLED (1223) error code.

An example of a UAC prompt for an unsigned application

With the runas verb specified, the application is now run in elevated mode, and the operating system asks the user for permission to continue. Unfortunately, if your application isn't signed, then you get a scarier version of the prompt, as displayed above. If your application is signed, then you'll get something similar to the screenshot below.

An example of a UAC prompt for a properly signed application

AngelCode bitmap font parsing using C#

$
0
0

The font parser library was used by this OpenGL application that renders text

While writing some bitmap font processing for an OpenGL project, I settled on using AngelCode's BMFont utility to generate both the textures and the font definition. However, this means I then needed to write a parser in order to use this in my OpenGL solution.

This library is a generic parser for the BMFont format - it doesn't include any rendering functionality or exotic references and should be usable in any version of .NET from 2.0 upwards. BMFont can generate fonts in three formats - binary, text and XML. The library currently supports text and XML, I may add binary support at another time; but currently I'm happy using the text format.

Note: This library only provides parsing functionality for loading BMFont data. It is up to you to provide functionality used to load textures and render characters

Overview of the library

There are four main classes used to describe a font:

  • BitmapFont - the main class representing the font and its attributes
  • Character - representing a single character
  • Kerning - represents the kerning between a pair of characters
  • Page - represents a texture page

There is also a support class, Padding, as I didn't want to reference System.Windows.Forms in order to use its own and using a Rectangle instead would be confusing. You can replace with this System.Windows.Forms version if you want.

Finally, the BitmapFontLoader class is a static class that will handle the loading of your fonts.

Loading a font

To load a font, call BitmapFontLoader.LoadFontFromFile. This will attempt to auto detect the file type and load a font. Alternatively, if you already know the file type in advance, then call the variations BitmapFontLoader.LoadFontFromTextFile or BitmapFontLoader.LoadFontFromXmlFile.

Each of these functions returns a new BitmapFont object on success.

Using a font

The BitmapFont class returns all the information specified in the font file, such as the attributes used to create the font. Most of these not directly used and are there only for if your application needs to know how the font was generated (for example if the textures are packed or not). The main things you would be interested in are:

  • Characters - this property contains all the characters defined in the font.
  • Kernings - this property contains all kerning definitions. However, mostly you should use the GetKerning method to get the kerning for a pair of characters.
  • Pages -this property contains the filenames of the textures used by the font. You'll need to manually load the relevant textures.
  • LineHeight - this property returns the line height. When rending text across multiple lines, use this property for incrementing the vertical coordinate - don't just use the largest character height or you'll end up with inconsistent line heights.

The Character class describes a single character. Your rendering functionality will probably need to use all of the properties it contains:

  • Bounds - the location and size of the character in the source texture.
  • TexturePage - the index of the page containing the source texture.
  • Offset - an offset to use when rendering the character so it lines up correctly with other characters.
  • XAdvance - the value to increment the horizontal coordinate by. Don't forgot to combine this value with the result of a call to GetKerning.

Example rendering using GDI

The sample project which accompanies this article shows a very basic way of rending using GDI; however this is just for demonstration purposes and you should probably come up with something more efficient in a real application!

Example rendering using the bitmap font viewer

    private void DrawCharacter(Graphics g, Character character, int x, int y)
    {
      g.DrawImage(_textures[character.TexturePage], new RectangleF(x, y, character.Bounds.Width, character.Bounds.Height), character.Bounds, GraphicsUnit.Pixel);
    }

    private void DrawPreview()
    {
      Size size;
      Bitmap image;
      string normalizedText;
      int x;
      int y;
      char previousCharacter;

      previousCharacter = ' ';
      normalizedText = _font.NormalizeLineBreaks(previewTextBox.Text);
      size = _font.MeasureFont(normalizedText);

      if (size.Height != 0 && size.Width != 0)
      {
        image = new Bitmap(size.Width, size.Height);
        x = 0;
        y = 0;

        using (Graphics g = Graphics.FromImage(image))
        {
          foreach (char character in normalizedText)
          {
            switch (character)
            {
              case '\n':
                x = 0;
                y += _font.LineHeight;
                break;
              default:
                Character data;
                int kerning;

                data = _font[character];
                kerning = _font.GetKerning(previousCharacter, character);

                this.DrawCharacter(g, data, x + data.Offset.X + kerning, y + data.Offset.Y);

                x += data.XAdvance + kerning;
                break;
            }

            previousCharacter = character;
          }
        }

        previewImageBox.Image = image;
      }
    }

The Bitmap Font Viewer application

This sample application loads and previews bitmap fonts

Also included in the download for this article is a simple Windows Forms application for viewing a bitmap font.

Note: All of the fonts I have created and tested were unpacked. The font viewer does not support packed textures, and while it will still load the font, it will not draw glyphs properly as it isn't able to do any of the magic with channels that the packed texture requires. In addition, as .NET doesn't support the TGA format by default, neither does this sample project.

Final Thoughts

Unlike my other articles, I haven't really gone into the source code or pointed out how it works, however it should all be simple to understand and use (despite having virtually no documentation) - please let me know if you think otherwise!

As mentioned above, I'm currently not using packed textures. The font parser will give you all the information you need regarding channels for extracting the information, but could probably be nicer done, such as using enums instead of magic ints - I may address this in a future update, along side implementing the binary file format.

Ideally the best way to use this code would be to inherit or extend the BitmapFont class. Therefore it would probably be better directly embedding the source code into your application, change the namespaces to match your own solution, then build from there.

I haven't tested with many fancy fonts - it's probable that the MeasureFont method doesn't handle cases of fonts with have a larger draw area than their basic box size.

Updates to this project will be posted to either CodePlex or GitHub - this article will be updated once the code is up.

Downloads

Converting 2D arrays to 1D and accessing as either 2D or 1D

$
0
0

While working on a recent gaming project, I was originally using 2D arrays to store information relating to the different levels in the game. But when it came to loop through the contents of these levels, it wasn't as straightforward to do a simple foreach loop due to the multiple dimensions.

Instead, I changed the code so that the 2D data was stored in a single dimension array. By using row-major order you can calculate any position in 2D space and map it into the 1D array. This then allows you to continue accessing the data using 2D co-ordinates, but opens up 1D access too.

Defining your array

Given the size of your 2D array, the 1D creation code is trivial:

T[] items = new T[width * height];

Converting 2D co-ordinates into 1D index

Once your have your array, converting a 2D co-ordinate such as 3, 4 into the correct index of your 1D array using row-major order using the following formula:

y * width + x

Converting 1D index into 2D co-ordinates

The calculation to convert a 1D index into a 2D co-ordinate is fairly straightforward:

y = index / width;
x = index - (y * height);

Putting it together - the ArrayMap<T> class

To avoid constantly having to repeat the calculations, I created a generic ArrayMap class that I could use to store any data type in a 1D array, and access the values using either indexes or co-ordinates, as well as adding enumerable support. The class is very straightforward to use:

ArrayMap<int> grid;
Size size;
int value;

size = new Size(10, 10);
value = 0;
grid = new ArrayMap<int>(size);

// set values via 2D co-ordinates
for (int y = 0; y < size.Height; y++)
{
  for (int x = 0; x < size.Width; x++)
  {
    grid[x, y] = value;
    value++;
  }
}

// get values via 2D co-ordinates
Console.WriteLine(grid[9, 0]); // 9
Console.WriteLine(grid[0, 9]); // 90
Console.WriteLine(grid[9, 9]); // 99

// set values via indexes
for (int i = 0; i < grid.Count; i++)
  grid[i] = i;

// get values via index
Console.WriteLine(grid[9]); // 9
Console.WriteLine(grid[90]); // 90
Console.WriteLine(grid[99]); // 99

// enumerate items
foreach (int i in grid)
  Console.WriteLine(i);

// get index
Console.WriteLine(grid.GetItemIndex(9, 9)); // 99

// get location
Console.WriteLine(grid.GetItemLocation(99)); // 9,9

Below is the full source to the class.

using System;
using System.Collections;
using System.Collections.Generic;

namespace BinaryRealms.Engine
{
  public class ArrayMap<T> : IEnumerable<T>
  {
    private T[] _items;
    private Size _size;

    public ArrayMap()
    { }

    public ArrayMap(int width, int height)
      : this(new Size(width, height))
    { }

    public ArrayMap(Size size)
      : this()
    {
      this.Size = size;
    }

    public IEnumerator<T> GetEnumerator()
    {
      foreach (T item in _items)
        yield return item;
    }

    public int GetItemIndex(int x, int y)
    {
      if (x < 0 || x >= this.Size.Width)
        throw new IndexOutOfRangeException("X is out of range");
      else if (y < 0 || y >= this.Size.Height)
        throw new IndexOutOfRangeException("Y is out of range");

      return y * this.Size.Width + x;
    }

    public int GetItemIndex(Point point)
    {
      return this.GetItemIndex(point.X, point.Y);
    }

    public Point GetItemLocation(int index)
    {
      Point point;

      if (index < 0 || index >= _items.Length)
        throw new IndexOutOfRangeException("Index is out of range");

      point = new Point();
      point.Y = index / this.Size.Width;
      point.X = index - (point.Y * this.Size.Height);

      return point;
    }

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

    public Size Size
    {
      get { return _size; }
      set
      {
        _size = value;
        _items = new T[_size.Width * _size.Height];
      }
    }

    public T this[Point location]
    {
      get { return this[location.X, location.Y]; }
      set { this[location.X, location.Y] = value; }
    }

    public T this[int x, int y]
    {
      get { return this[this.GetItemIndex(x, y)]; }
      set { this[this.GetItemIndex(x, y)] = value; }
    }

    public T this[int index]
    {
      get { return _items[index]; }
      set { _items[index] = value; }
    }

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

Currently I'm using this class without any problems, but if you spot any errors or think it could do with anything else, please let me know!


Creating an image viewer in C# Part 5: Selecting part of an image

$
0
0
A newer version of this code is now available.

Part 4 of this series (by far the most popular article on cyotek.com) was supposed to be the end, but recently I was asked if was possible to select part of an image for saving it to a file. After implementing the new functionality and lacking ideas for a new post on other matters, here we are with a new part!

The demonstration program showing the selection functionality

Getting Started

If you aren't already familiar with the ImageBox component, you may wish to view parts 1, 2, 3 and 4 for the original background and specification of the control.

First thing is to add some new properties, along with backing events. These are:

  • SelectionMode - Determines if selection is available within the control
  • SelectionColor - Primary color for drawing the selection region
  • SelectionRegion - The currently selected region.
  • LimitSelectionToImage - This property allows you to control if the selection region can be drawn outside the image boundaries.
  • IsSelecting - This property returns if a selection operation is in progress

If the SelectionMode property is set, then the AutoPan and AllowClickZoom properties will both be set to false to avoid conflicting actions.

We also need a couple of new events not directly tried to properties.

  • Selecting - Occurs when the user starts to draw a selection region and can be used to cancel the action.
  • Selected - Occurs when the user completes drawing a selection region

These events are called when setting the IsSelecting property:

[Browsable(false), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
public virtual bool IsSelecting
{
  get { return _isSelecting; }
  protected set
  {
    if (_isSelecting != value)
    {
      CancelEventArgs args;

      args = new CancelEventArgs();

      if (value)
        this.OnSelecting(args);
      else
        this.OnSelected(EventArgs.Empty);

      if (!args.Cancel)
        _isSelecting = value;
    }
  }
}

Drawing the selection highlight

Before adding support for defining the selection region, we'll add the code to draw it - that way we'll know the code to define the region works! To do this, we'll modify the existing OnPaint override, and insert a call to a new method named DrawSelection:

protected override void OnPaint(PaintEventArgs e)
{
  /* Snipped existing code for brevity */

  // draw the selection
  if (this.SelectionRegion != Rectangle.Empty)
    this.DrawSelection(e);

  base.OnPaint(e);
}

The DrawSelection method itself is very straightforward. First it fills the region with a translucent variant of the SelectionColor property, then draws a solid outline around this. A clip region is also applied to avoid overwriting the controls borders.

As with most of the methods and properties in the ImageBox control, it has been marked as virtual to allow you to override it and provide your own drawing implementation if required, without needing to redraw all of the control.

protected virtual void DrawSelection(PaintEventArgs e)
{
  RectangleF rect;

  e.Graphics.SetClip(this.GetInsideViewPort(true));

  rect = this.GetOffsetRectangle(this.SelectionRegion);

  using (Brush brush = new SolidBrush(Color.FromArgb(128, this.SelectionColor)))
    e.Graphics.FillRectangle(brush, rect);

  using (Pen pen = new Pen(this.SelectionColor))
    e.Graphics.DrawRectangle(pen, rect.X, rect.Y, rect.Width, rect.Height);

  e.Graphics.ResetClip();
}

The GetOffsetRectangle method will be described a little further down this article.

Defining the selection region

Currently the selection region can only be defined via the mouse; there is no keyboard support. To do this, we'll do the usual overriding of MouseDown, MouseMove and MouseUp

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

  /* Snipped existing code for brevity */

  if (e.Button == MouseButtons.Left && this.SelectionMode != ImageBoxSelectionMode.None)
    this.SelectionRegion = Rectangle.Empty;
}

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

  if (e.Button == MouseButtons.Left)
  {
    /* Snipped existing code for brevity */
  
    this.ProcessSelection(e);
  }
}

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

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

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

OnMouseDown and OnMouseUp aren't being used for much in this case, the former is used to clear an existing selection region, the later to notify that the selection is no longer being defined. OnMouseMove calls the ProcessSelection method which is where all the action happens.

protected virtual void ProcessSelection(MouseEventArgs e)
{
  if (this.SelectionMode != ImageBoxSelectionMode.None)
  {
    if (!this.IsSelecting)
    {
      _startMousePosition = e.Location;
      this.IsSelecting = true;
    }

First, we check to make sure a valid selection mode is set. Then, if a selection operation hasn't been initiated, we attempt to set the IsSelecting property. As noted above, this property will call the Selecting event allowing the selection to be cancelled if required by the implementing application.

    if (this.IsSelecting)
    {
      float x;
      float y;
      float w;
      float h;
      Point imageOffset;

      imageOffset = this.GetImageViewPort().Location;

      if (e.X < _startMousePosition.X)
      {
        x = e.X;
        w = _startMousePosition.X - e.X;
      }
      else
      {
        x = _startMousePosition.X;
        w = e.X - _startMousePosition.X;
      }

      if (e.Y < _startMousePosition.Y)
      {
        y = e.Y;
        h = _startMousePosition.Y - e.Y;
      }
      else
      {
        y = _startMousePosition.Y;
        h = e.Y - _startMousePosition.Y;
      }

      x = x - imageOffset.X - this.AutoScrollPosition.X;
      y = y - imageOffset.Y - this.AutoScrollPosition.Y;

If selection was allowed, we construct the co-ordinates for a rectangle, automatically switching values around to ensure that the rectangle will always have a positive width and height. We'll also offset the co-ordinates if the image has been scrolled or if it has been centred (or both!).

      x = x / (float)this.ZoomFactor;
      y = y / (float)this.ZoomFactor;
      w = w / (float)this.ZoomFactor;
      h = h / (float)this.ZoomFactor;

As this is the zoomable scrolling image control, we also need to rescale the rectangle according to the current zoom level. This ensures the SelectionRegion property always returns a rectangle that describes the selection at 100% zoom.

      if (this.LimitSelectionToImage)
      {
        if (x < 0)
          x = 0;

        if (y < 0)
          y = 0;

        if (x + w > this.Image.Width)
          w = this.Image.Width - x;

        if (y + h > this.Image.Height)
          h = this.Image.Height - y;
      }

      this.SelectionRegion = new RectangleF(x, y, w, h);
    }
  }
}

The final step is to constrain the rectangle to the image size if the LimitSelectionToImage property is set, before assigning the final rectangle to the SelectionRegion property.

And that's pretty much all there is to it.

Scaling and offsetting

When using the control in our own products, it's very rarely to display a single image, but rather to display multiple items, be it sprites in a sprite sheet or tiles in a map. These implementations therefore often require the ability to get a single item, for example to display hover effects. This can be tricky with a control that scrolls, zooms and centres the image. Rather than repeat ZoomFactor calculations (and worse AutoScrollPosition) everywhere, we added a number of helper methods named GetOffset* and GetScaled*. Calling these with a "normal" value, will return that value repositioned and rescaled according to the current state of the control. An example of this is the DrawSelection method described above which needs ensure the current selection region is rendered correctly.

public virtual RectangleF GetScaledRectangle(RectangleF source)
{
  return new RectangleF
    (
      (float)(source.Left * this.ZoomFactor),
      (float)(source.Top * this.ZoomFactor),
      (float)(source.Width * this.ZoomFactor),
      (float)(source.Height * this.ZoomFactor)
    );
}

public virtual RectangleF GetOffsetRectangle(RectangleF source)
{
  RectangleF viewport;
  RectangleF scaled;
  float offsetX;
  float offsetY;

  viewport = this.GetImageViewPort();
  scaled = this.GetScaledRectangle(source);
  offsetX = viewport.Left + this.Padding.Left + this.AutoScrollPosition.X;
  offsetY = viewport.Top + this.Padding.Top + this.AutoScrollPosition.Y;

  return new RectangleF(new PointF(scaled.Left + offsetX, scaled.Top + offsetY), scaled.Size);
}

Versions of these methods exist for the following structures:

  • Point
  • PointF
  • Size
  • SizeF
  • Rectangle
  • RectangleF

These methods can come in extremely useful depending on how you are using the control!

Cropping an image

The demonstration program displays two ImageBox controls, the first allows you to select part of an image, and the second displays the cropped selection. I didn't add any sort of crop functionality to the control itself, but the following snippets shows how the demonstration program creates the cropped version.

Rectangle rect;

if (_previewImage != null)
  _previewImage.Dispose();

rect = new Rectangle((int)imageBox.SelectionRegion.X, (int)imageBox.SelectionRegion.Y, (int)imageBox.SelectionRegion.Width, (int)imageBox.SelectionRegion.Height);

_previewImage = new Bitmap(rect.Width, rect.Height);

using (Graphics g = Graphics.FromImage(_previewImage))
  g.DrawImage(imageBox.Image, new Rectangle(Point.Empty, rect.Size), rect, GraphicsUnit.Pixel);
}

previewImageBox.Image = _previewImage;

Finishing touches

We'll finish off by adding a couple of helper methods that implementers can call:

public virtual void SelectAll()
{
  if (this.Image == null)
    throw new InvalidOperationException("No image set");

  this.SelectionRegion = new RectangleF(PointF.Empty, this.Image.Size);
}

public virtual void SelectNone()
{
  this.SelectionRegion = RectangleF.Empty;
}

Known issues

Currently, if you try and draw the selection bigger than the visible area of the control, it will work, but it will not scroll the control for you. I also was going to add the ability to move or modify the selection but ran out of time for this particular post.

As always, if you have any comments or questions, please contact us!

Downloads

Arcade explosion generator

$
0
0

Over the past few weeks I've been messing around creating a unique graphics for our Jewel Rush game. One of the things I was experimenting with was explosion animations. Although tools exist for generating explosions the problem with most of these is that they create large sprites which don't shrink well, and the output is a bit more realistic than what I was looking for.

And while I'm competent enough to do application graphics (more or less!), gaming graphics are a completely different kettle of fish!

A screenshot from Missile Command

Above is a screenshot from Missile Command, a classic from Atari. That's the sort of explosions I wanted to create, so I wrote a small tool that would create these sort of graphics in a random (but reproducible) fashion and export them to images for use in other tools such as Spriter. As it turned out, the graphics it produces didn't end up quite that way (I was having problems with the intersection stuff) but it's usable enough for the purposes I want.

A sample of the default output

Another sample using some custom settings.

The application was thrown together over the weekend so it's probably not hugely robust and may contain a small army of bugs. But it works and is possibly an interesting starting point for other projects. There's some interesting bits of code here and there, although I'm not writing about the implementation of the code.

Application Features

  • Configuration settings can be saved and reloaded for tweaking of favoured settings. Uses basic Reflection serialization as XmlSerializer can't handle colors without having to create duplicate color properties in string format.
  • Can export either a complete sprite sheet, or the individual images
  • Copy the sprite sheet to the clipboard (although I noticed that transparent doesn't work if you do, something to look at later)
  • Uses the ImageBox (of course!) for displaying previews
  • The TrackBar control embedded in the ToolStrip is a custom component inheriting from ToolStripControlHost which can be reused. And once you understand the principles, it's so easy to host other controls.

Graphic Settings

  • Either specify a seed to always recreate the same explosion, or use a random seed each time. (If you find a seed you like, clicking the seed number in the status bar will apply it to your configuration settings).
  • Specify the number of animation frames that will be generated, and the size of the frames
  • Specify the maximum number of explosion booms available at once. There's also an option to automatically remove and recreate "expired" blooms.
  • Choose the colors used to render the bloms
  • Specify the percentage by which blooms grow (and shrink), and how many growth states there are. Once a bloom has shrunk to its minimum size, it is expired and no longer draw.
  • Anti alias options, useful if you don't want pixel graphics
  • Border size and growth
  • Set a random order, in which newly created blooms will be inserted randomly in the list.
  • An experimental mask mode which was supposed to enable me to create those Missile Command style XOR drawing. However, it doesn't really work and I'll probably have another go at it at some point.

An example generated by the projectAnother example of generated output

Room for improvement

Everything can be improved, one of the ideas I'd had for this tool was greater control over blooms, allowing you configure their locations etc with better precision but it wasn't necessary for the graphic I was creating. As mentioned above, the masking doesn't work as expected, it would have been nice if it did. Some better rendering would be a plus too, at the moment the "explosions" are simple rings of color. Some noise or other minor particle effects to make them a little less uniform would probably look interesting.

Source Code

Source code, and optionally pre-compiled binaries, are available from the link below. The code has been compiled against the .NET 3.5 Client Profile. Due to some minor use of Linq and auto generated properties a small amount of work would be needed to compile against .NET 2.0. I'm afraid comments are somewhat lacking as well, I wasn't planning on releasing this publicly originally.

If anyone creates any interesting graphics or improves upon the code, we'd love to hear from you.

Downloads

Displaying the contents of a PDF file in an ASP.NET application using GhostScript

$
0
0

After receiving quite a few requests on making the PDF image conversion work in a web application, I wanted to see how hard it would be to do. Not hard at all as it turns out, I had a nice working sample running with a bare 5 minutes of work.

The sample available for download below is a basic ASP.NET application, comprised of a single page with an IHttpHandler for displaying the image. In order to make this sample as easy as possible, it uses pure server side controls and code, nothing client side.

Getting Started

In order to run this sample, you'll need the Cyotek.GhostScript and Cyotek.GhostScript.PdfConversion.zip components described in a previous article.

You'll also need to download GhostScript. As with my other articles on the subject, please make sure you check their license terms - they seem very keen that people don't use the GPL version or distribute GhostScript without a commercial license.

Locating gsdll32.dll

In order for this to work, gsdll32.dll needs to be somewhere in your applications path. This could be in your system32 directory on 32bit Windows, or SysWOW64 on 64bit Windows.

While developing this sample, I also tried having the file in the bin directory of the website - this also worked fine. However, as the website was running on my local machine, it's probably running in Full Trust, and I have no idea if it will work in Medium Trust or lower.

I'm running 64bit Windows!

Congratulations! I have nothing but issues with 32bit web servers. But I digress. The sample projects I have provided on this website all use the 32bit version of GhostScript. There is a 64bit version available, but I haven't downloaded it to test. Your options should be as follows:

  • Build against the 64bit GhostScript DLL. This may need some refactoring if their public API has changed. At the very least, you'll need to change the DLL filename in the native method calls.
  • Using IIS7 or higher? Keep using the 32bit version, and set your worker pool to run in 32bit mode
  • Using IIS6? Commiserations, I feel your pain. The only option here, if you stay 32bit, is to have the entire IIS run as 32bit.

I have tested on a Windows 7 Professional 64bit machine as follows:

  • Firstly, using IISExpress which is running as a 32bit process
  • Secondly, using IIS7 with a custom application pool running in 32bit mode

Both of these scenarios worked perfectly well.

Creating the solution

Create a new ASP.NET Web Forms Site

Note: Even though this example uses pure WebForms, there's no reason that this sort of code won't work fine in ASP.NET MVC or any other .NET framework of your choice.

Open up Default.aspx and add some controls similar to the following:

<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="Default.aspx.cs" Inherits="GhostScriptWebTest._Default" %><!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"><html xmlns="http://www.w3.org/1999/xhtml"><head runat="server"><title>PDF Conversion Example</title></head><body><form id="form1" runat="server"><div><p><asp:LinkButton runat="server" ID="previousLinkButton" Text="Previous" OnClick="previousLinkButton_Click" /><asp:LinkButton runat="server" ID="nextLinkButton" Text="Next" OnClick="nextLinkButton_Click" /></p><p><asp:Image runat="server" ID="pdfImage" ImageUrl="~/PdfImage.ashx?fileName=sample.pdf&page=1" /></p></div></form></body></html>

The controls should be fairly self explanatory! The main thing of interest is the pdfImage Image control - this will call a Generic Handler that I'll describe in the next section. Note that VS2010 and VS2012 have another option, an ASP.NET Handler - this implements the same IHttpHandler interface but doesn't have a .ashx file and is registered differently. If you are using IIS7 or above, you're probably better off using that.

Note that by default the pdfImage control is pointing to a sample file named sample.pdf - add any old PDF to the root of your website and name it sample. Ensure that the Build Action for the PDF is set to Content, otherwise it won't be deployed with your application.

Creating the image handler

Tutorials on creating image handlers with IHttpHandler can be found scattered throughout the net, so I'll not go into how they work, but just describe the implementation I'm using in this example. Add a new generic handler to your project, then fill in the ProcessRequest method as follows. Make sure you add the two GhostScript API components to your solution and add references to them to your web application first!

using System;
using System.Drawing;
using System.Drawing.Imaging;
using System.Web;
using Cyotek.GhostScript.PdfConversion;

namespace GhostScriptWebTest
{
  public class PdfImage : IHttpHandler
  {
    public void ProcessRequest(HttpContext context)
    {
      string fileName;
      int pageNumber;
      Pdf2Image convertor;
      Bitmap image;

      fileName = context.Server.MapPath("~/" + context.Request.QueryString["fileName"]);
      pageNumber = Convert.ToInt32(context.Request.QueryString["page"]);

      // convert the image
      convertor = new Pdf2Image(fileName);
      image = convertor.GetImage(pageNumber);

      // set the content type
      context.Response.ContentType = "image/png";

      // save the image directly to the response stream
      image.Save(context.Response.OutputStream, ImageFormat.Png);
    }

    public bool IsReusable
    { get { return true; } }
  }
}

Again, this is extremely simple code. I extract the query string of the request to obtain the file name of the PDF document to convert, and the page to display. I then create an instance of the Pdf2Image class, and grab an image of the specified page.

Next, you need to set the ContentType of the Response object so the web browser knows what to do with your content. Finally, I save the image directly to the response's OutputStream. Make sure that the format you save the image as matches the content type you've specified.

With these steps complete, building and running the website should present you with a pair of hyper links, and the first page of your PDF file as a static image. [Well, it will if you add a pair of blank event handlers for those defined for the two hyperlink buttons anyway]

Simple navigation

Now that we can display our PDF, we'll add some basic navigation. Open up the code behind file for Default.aspx and fill in the event handlers for the two hyperlink buttons.

using System;
using System.Collections.Specialized;
using System.Web;
using Cyotek.GhostScript.PdfConversion;

namespace GhostScriptWebTest
{
  public partial class _Default : System.Web.UI.Page
  {
    protected void previousLinkButton_Click(object sender, EventArgs e)
    {
      this.IncrementPage(-1);
    }

    protected void nextLinkButton_Click(object sender, EventArgs e)
    {
      this.IncrementPage(1);
    }

    private void IncrementPage(int increment)
    {
      NameValueCollection queryString;
      int pageNumber;
      string pdfFileName;
      Pdf2Image converter;

      queryString = HttpUtility.ParseQueryString(pdfImage.ImageUrl.Substring(pdfImage.ImageUrl.IndexOf("?")));
      pdfFileName = queryString["fileName"];
      pageNumber = Convert.ToInt32(queryString["page"]) + increment;
      converter = new Pdf2Image(this.Server.MapPath("~/" + pdfFileName));

      if (pageNumber > 0 && pageNumber <= converter.PageCount)
        pdfImage.ImageUrl = string.Format("~/PdfImage.ashx?fileName={0}&page={1}", pdfFileName, pageNumber);
    }
  }
}

As with the image handler, this code simply extracts the file name of the PDF file and the current page number. It also creates a new instance of the Pdf2Image class in order to obtain the number of pages in the document. If the new page number is in range, it updates the ImageUrl of the pdfImage causing the image handler to pull back the next page.

In Conclusion

This sample is pretty inefficient and at the very least should be caching the images. But, it's as simple an example as I can make. Hopefully someone will find it useful. At the present time I'm not working with the GhostScript API library so I suspect this will be the last article on the subject for the time being.

Downloads

Creating a multi-paged container control with design time support

$
0
0

This article describes adding design time support for a TabControl-like component which renders the same way the Project Properties in Visual Studio 2012.

The TabList demonstration application.

This is the first time I've tried to make more advanced use of component designers so there are going to be areas that I'm not aware of or have not implemented correctly. The component seems to be working fine, but it's entirely possible that bugs exist, which could cause problems. Caveat emptor!

Overview of the control

For this article, I'm not going to delve into how the control itself was put together as I want to focus on the design time support, so I'm just going to provide a quick overview.

  • TabList - the main control
  • TabListPage - these are hosted by the TabList to provided multi-paged support
  • TabListControlCollection - a custom ControlCollection that handles TabListPages, and prevents adding other controls directly onto the TabList
  • TabListPageCollection - a strongly typed wrapper for TabListPage objects

The basics of these four classes are all based on the TabControl. If you know how to use that, then you know how to use the TabList control, some property names have changed but otherwise it's pretty similar.

For rendering support, we use these classes:

  • ITabListPageRenderer - interface to be implemented by rendering classes
  • TabListPageRenderer - base class to inherit for render support, and also provides a default renderer property
  • TabListPageState - flags which describe the state of a TabListPage
  • DefaultTabListPageRenderer - simple renderer which draws a header in a Visual Studio 2012-esque style.

And finally, we have the two designers which this article will concentrate on:

  • TabListDesigner - designer class for the TabList control
  • TabListPageDesigner - designer class for the TabListPage control

Implementing the TabListDesigner

As the TabList control is a container control, we can't use the base ControlDesigner. Instead, we'll use ParentControlDesigner which has a bunch of extra functionality we need.

Initializing a new control

Normally, I initialize a component via the constructor of the control. This is fine when you're initializing properties to default values, but what about adding child items? Consider for example the TabControl. When add one of these to a form, it generates two hosted pages. If you remove these, they don't come back. If you've ever looked at the designer generated code for a control, you'll see it will add items to a collection, but doesn't clear the collection first so creating items via the initialization method of a component would be problematic.

Fortunately for us, the designer has two methods you can override. InitializeNewComponent is called when you create a new instance of the designed type. InitializeExistingComponent can be used to modify an existing component. There's also a third override, InitializeNonDefault although I'm not sure when this is called.

For our purposes, overriding the InitializeNewComponent method is enough:

public override void InitializeNewComponent(IDictionary defaultValues)
{
  base.InitializeNewComponent(defaultValues);
  // add two default pages to each new control and reset the selected index
  this.AddTabListPage();
  this.AddTabListPage();
  this.TabListControl.SelectedIndex = 0;
}

Now, whenever you add a TabList control onto a designer surface such as a Form, it'll get two shiny new TabListPages.

Hooking up events

For our designer, we need to know when certain actions occur so we can act accordingly - for example, to disable the Remove verb if there's nothing to remove. We'll set these up by overriding the Initialize method.

public override void Initialize(IComponent component)
{
  TabList control;
  ISelectionService selectionService;
  IComponentChangeService changeService;

  base.Initialize(component);

  // attach an event so we can be notified when the selected components in the host change
  selectionService = (ISelectionService)this.GetService(typeof(ISelectionService));
  if (selectionService != null)
    selectionService.SelectionChanged += this.OnSelectionChanged;

  // attach an event to notify us of when a component has been modified
  changeService = (IComponentChangeService)this.GetService(typeof(IComponentChangeService));
  if (changeService != null)
    changeService.ComponentChanged += this.OnComponentChanged;

  // attach an event so we can tell when the SelectedIndex of the TabList control changes
  control = component as TabList;
  if (control != null)
    control.SelectedIndexChanged += this.OnSelectedIndexChanged;
}

OnSelectionChanged

The first event we attached as ISelectionService.SelectionChanged. This event is raised when the selected components change. We'll use this event to automatically activate a given TabListPage if a control hosted upon it is selected.

private void OnSelectionChanged(object sender, EventArgs e)
{
  ISelectionService service;

  service = (ISelectionService)this.GetService(typeof(ISelectionService));
  if (service != null)
  {
    TabList control;

    control = this.TabListControl;
    foreach (object component in service.GetSelectedComponents())
    {
      TabListPage ownedPage;

      // check to see if one of the selected controls is hosted on a TabListPage. If it is, 
      // activate the page. This means, if for example, you select a control via the
      // IDE's properties window, the relavent TabListPage will be activated

      ownedPage = this.GetComponentOwner(component);
      if (ownedPage != null && ownedPage.Parent == control)
      {
        control.SelectedPage = ownedPage;
        break;
      }
    }
  }
}

OnComponentChanged

The second event IComponentChangeService.ComponentChanged is raised when the RaiseComponentChanged method is called. We'll describe how this method works a bit further on, but for now, we use the event to determine if there are any tab pages in the control - if there are, the remove command is enabled, otherwise it's disabled. (We'll also describe the verbs further down too!)

private void OnComponentChanged(object sender, ComponentChangedEventArgs e)
{
  // disable the Remove command if we dont' have anything we can actually remove
  if (_removeVerb != null)
    _removeVerb.Enabled = this.TabListControl.TabListPageCount > 0;
}

OnSelectedIndexChanged

The final event, TabList.SelectedIndexChanged is on the TabList control itself. We use this event to select the TabList component for designing due to how component selection seems to work when you mix runtime and design time functionality.

private void OnSelectedIndexChanged(object sender, EventArgs e)
{
  ISelectionService service;

  service = (ISelectionService)this.GetService(typeof(ISelectionService));
  if (service != null)
  {
    // set the TabList control as the selected object. We need to do this as if the control is selected as a result
    // of GetHitTest returning true, normal designer actions don't seem to take place
    // Alternatively, we could select the selected TabListPage instead but might as well stick with the standard behaviour
    service.SetSelectedComponents(new object[] { this.Control });
  }
}

Verbs

I mentioned verbs above, but just what are they? Well, they are commands you attach to the context and tasks menu of controls. To do this, override the Verbs property of your designer and create a verbs collection.

public override DesignerVerbCollection Verbs
{
  get
  {
    if (_verbs == null)
    {
      _verbs = new DesignerVerbCollection();

      _addVerb = new DesignerVerb("Add TabListPage", this.AddVerbHandler) { Description = "Add a new TabListPage to the parent control." };
      _removeVerb = new DesignerVerb("Remove TabListPage", this.RemoveVerbHandler) { Description = "Remove the currently selected TabListPage from the parent control." };

      _verbs.Add(_addVerb);
      _verbs.Add(_removeVerb);
    }

    return _verbs;
  }
}

Each verb binds to an event handler. For our purposes the events are simple and just pass through into other methods.

private void AddVerbHandler(object sender, EventArgs e)
{
  this.AddTabListPage();
}

private void RemoveVerbHandler(object sender, EventArgs e)
{
  this.RemoveSelectedTabListPage();
}

I suppose you could just use an anonymous delegate instead.

Modifying a component with undo support

If you are making multiple changes a control, and one of these goes wrong, the IDE won't automatically undo the changes for you and you will need to handle this yourself. Fortunately, the IDE does provide the facility via designer transactions. In additional to providing a single undo for a number of operations, using transactions can also be good for performance as UI updates are delayed until the transaction is complete.

The code below is called by the Add verb and adds a new TabListPage to the control.

These are the basic steps for making changes:

  • Create a transaction via IDesignerHost.CreateTransaction
  • Notify the designer of impending changes via the RaiseComponentChanging method
  • Make the change
  • Notify the designer that the change has been made via the RaiseComponentChanged method. This will raise the IComponentChangeService.ComponentChanged event mentioned above.
  • Either Commit or Cancel the transaction

In this case, despite wrapping the transaction in a using statement, I've got got an explicit trycatch block to cancel the transaction in the event of an error. I'm not sure if this is strictly necessary however.

protected virtual void AddTabListPage()
{
  TabList control;
  IDesignerHost host;

  control = this.TabListControl;
  host = (IDesignerHost)this.GetService(typeof(IDesignerHost));

  if (host != null)
  {
    using (DesignerTransaction transaction = host.CreateTransaction(string.Format("Add TabListPage to '{0}'", control.Name)))
    {
      try
      {
        TabListPage page;
        MemberDescriptor controlsProperty;

        page = (TabListPage)host.CreateComponent(typeof(TabListPage));
        controlsProperty = TypeDescriptor.GetProperties(control)["Controls"];

        // tell the designer we're about to start making changes
        this.RaiseComponentChanging(controlsProperty);

        // set the text to match the name
        page.Text = page.Name;

        // add the new control to the parent, and set it to be the active page
        control.Controls.Add(page);
        control.SelectedIndex = control.TabListPageCount - 1;

        // inform the designer we're finished making changes
        this.RaiseComponentChanged(controlsProperty, null, null);

        // commit the transaction
        transaction.Commit();
      }
      catch 
      {
        transaction.Cancel();
        throw;
      }
    }
  }
}

The handler for the remove verb does pretty much the same thing, except we use IDesignerHost.DestroyComponent to remove the selected TabListPage control.

protected virtual void RemoveSelectedTabListPage()
{
  TabList control;

  control = this.TabListControl;

  if (control != null && control.TabListPageCount != 0)
  {
    IDesignerHost host;

    host = (IDesignerHost)this.GetService(typeof(IDesignerHost));

    if (host != null)
    {
      using (DesignerTransaction transaction = host.CreateTransaction(string.Format("Remove TabListPage from '{0}'", control.Name)))
      {
        try
        {
          MemberDescriptor controlsProperty;

          controlsProperty = TypeDescriptor.GetProperties(control)["Controls"];

          // inform the designer we're about to make changes
          this.RaiseComponentChanging(controlsProperty);

          // remove the tab page
          host.DestroyComponent(control.SelectedPage);

          // tell the designer w're finished making changes
          this.RaiseComponentChanged(controlsProperty, null, null);

          // commit the transaction
          transaction.Commit();
        }
        catch
        {
          transaction.Cancel();
          throw;
        }
      }
    }
  }
}

Adding controls to the selected TabListPage

If the TabList control is selected and you try to drag a control on it, you'll get an error stating that only TabListPage controls can be hosted. By overriding the CreateToolCore method, we can intercept the control creation, and forward it onto the current TabListPage via the InvokeCreateTool method.

protected override IComponent[] CreateToolCore(ToolboxItem tool, int x, int y, int width, int height, bool hasLocation, bool hasSize)
{
  TabList control;
  IDesignerHost host;

  control = this.TabListControl;

  // prevent controls from being created directly on the TabList
  if (control.SelectedPage == null)
    throw new ArgumentException(string.Format("Cannot add control '{0}', no page is selected.", tool.DisplayName));

  host = (IDesignerHost)this.GetService(typeof(IDesignerHost));
  if (host != null)
  {
    ParentControlDesigner childDesigner;

    childDesigner = (ParentControlDesigner)host.GetDesigner(control.SelectedPage);

    // add controls onto the TabListPage control instead of the TabList
    ParentControlDesigner.InvokeCreateTool(childDesigner, tool);
  }

  return null;
}

Returning null via CreateToolCore prevents the control from being created on the TabList. The reminder of the logic forwards the call onto the selected TabListPage, if one is available.

Allowing TabListPage selection at design-time

As you'll have noticed, most controls can't be used at design time - when you click a control it just selects it. This default behaviour is a serious problem for our component as if you can't active other pages, how can you add controls to them? Fortunately, this is extremely easy to implement as the designer provides a GetHitTest method which you can override. If you return true from this method, then mouse clicks will be processed by the underlying control instead of the designer.

protected override bool GetHitTest(Point point)
{
  TabList control;
  bool result;
  Point location;

  // return true if the mouse is located over a TabListPage header
  // this allows you to switch pages at design time with the mouse
  // rather than just selecting the control as it would otherwise

  control = this.TabListControl;
  location = control.PointToClient(point);
  result = control.HitTest(location) != null;

  return result;
}

In the above code, we translate the provided mouse co-ordinates into the client co-ordinates, then test to see if they are on the header of a TabListPage. If they are, we return true and the call will then be forward onto the TabList control which will then selected that page.

There is one side effect of this behaviour. As we have essentially intercepted the mouse call, that means the TabList control isn't selected. This behaviour is inconsistent with standard behaviour and this is why when the designer was initialized we hooked into the SelectedIndexChanged event of the TabList control. With this hooked, as soon as the SelectedIndex property is changed we can manually select the TabList control. Of course, if you'd rather, you could change that code to select the active TabListPage instead, but again that's inconsistent with standard behaviour.

Unfortunately there's also another side effect I discovered - the context menu no longer works if you right click on an area where you allow mouse clicks to pass through. Again, this is fairly straightforward to work around by overriding WndProc and intercepting the WM_CONTEXTMENU message.

protected override void WndProc(ref Message m)
{
  switch (m.Msg)
  {
    case 0x7b: // WM_CONTEXTMENU
      Point position;

      // For some reason the context menu is no longer displayed when right clicking the control
      // By hooking into the WM_CONTEXTMENU context message we can display the menu ourselves

      position = Cursor.Position;

      this.OnContextMenu(position.X, position.Y);
      break;
    default:
      base.WndProc(ref m);
      break;
  }
}

Note: Normally I wouldn't use "magic numbers" as I have here. But at the same time, I don't want to define WM_CONTEXTMENU in this class - for my internal projects, I link to an assembly I've created which contains all the Win32 API functionality that I use. Linking that to this not possible for this example and I don't want to create a Native class for a just a single member. So this time I'll cheat and leave an inline magic number.

The final side effect I've found is double clicking to open the default event handler doesn't work either.

Design time control paining

The final section of the TabListDesigner class I want to discuss is design time painting. Normally, in the OnPaint overriding of my control, I would have a block similar to the below.

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

  if (this.DesignMode)
  {
    // Design time painting here
  }
}

While there's nothing wrong with this approach, if you are using a designer than you have another option, which saves you having to do design time checks each time your contain is painted at runtime. The designer has an OnPaintAdornments method, just override this to perform your design time drawing.

protected override void OnPaintAdornments(PaintEventArgs pe)
{
  base.OnPaintAdornments(pe);

  // outline the control at design time as we don't have any borders
  ControlPaint.DrawFocusRectangle(pe.Graphics, this.Control.ClientRectangle);
}

As the TabList doesn't have a border property, I draw a dotted line around the control using ControlPaint.DrawFocusRectangle.

Implementing the TabListPage designer

Although the TabListPage control is basically a Panel control with a bunch of properties and events hidden, it still needs a designer to override some functionality. For the TabListPageDesigner class, we'll inherit from ScrollableControlDesigner.

Removing sizing and moving handles

As the TabList control takes care of sizing its child TabListPage controls, we don't really want the user to be able to resize or move them at design time. By overriding the SelectionRules property, you can define exactly which handles are displayed. As I don't want the control to be moved or sized, I get rid of everything via the Locked flag.

public override SelectionRules SelectionRules
{ get { return SelectionRules.Locked; } }

Preventing the component from being re-parented

The CanBeParentedTo method is used to determine if a component can be hosted by another control. I'm overriding this to make sure that they can only be parented on another TabList control. Although, as I've disabled the dragging of TabListPage controls with selection rules above, you can't drag them to reparent anyway.

public override bool CanBeParentedTo(IDesigner parentDesigner)
{
  return parentDesigner != null && parentDesigner.Component is TabList;
}

Known Issues

  • As described above, if you double click one of the TabListPage headers nothing happens. Normally, you'd expect a code window to be opened at the default event handler for the control. While it should be possible to trap the WM_LBUTTONDBLCLK message, I don't know how to open a code window, or create a default event handler is one is missing.
  • Another issue I spotted is that I can't Cut (or Copy) a TagListPage from one TabList control to another. Not sure why yet, but I'll update the source on GitHub when I fix it.

The source

Get the source code from the link below. I've also uploaded it to GitHub, feel free to fork and make pull requests to make this component even better!

Downloads

Zooming into a fixed point on a ScrollableControl

$
0
0

If I'd built subtitle support into the CMS that powers this website, then surely the subtitle would have been "or how I fixed that annoying zoom bug in the ImageBox control". And with that digression out of the way, onto the article, a nice and short one for a change!

I should probably point out that this article doesn't describe how to actually do any zooming (as that is dependant on what it is you are actually doing a zoom upon), but rather how to keep the viewport focused on a given point after zooming. To learn about zooming, please see previous articles that describe the ImageBox control in detail.

Users of the ImageBox control are probably aware of the zoom bug, where each time you use the mouse wheel to zoom in or out, the final image position is slightly offset, as shown by this short animation:

Mouse wheel zoom - subtly broken.

Fixing this bug is actually quite simple and I'm actually embarrassed at how long it took to fix and how I missed the solution for so long. The key to resolving this issue is finding out the document position under the mouse (by which I mean the position in the entire scroll area, not just the visible viewport) before applying the zoom, and then recalculating this position with the new zoom level, offset by the mouse position in the client control.

As it's probably easier just to show you the code rather than try and describe it, it results in this small function:

public virtual void ScrollTo(Point imageLocation, Point relativeDisplayPoint)
{
  int x;
  int y;

  x = (int)(imageLocation.X * this.ZoomFactor) - relativeDisplayPoint.X;
  y = (int)(imageLocation.Y * this.ZoomFactor) - relativeDisplayPoint.Y;

  this.AutoScrollPosition = new Point(x, y);
}

To use it, you add code similar to the following where you process mouse clicks, or mouse wheel, however you control zooming with the mouse:

Point cursorPosition;
Point currentPixel;
int currentZoom;

// TODO: Obtain cursor position from MouseEventArgs etc.

currentPixel = this.PointToImage(cursorPosition);
currentZoom = this.Zoom;

// TODO: Perform zoom here

if (this.Zoom != currentZoom)
  this.ScrollTo(currentPixel, cursorPosition);

So how does this work?

  1. Get the mouse cursor position, relative to the control
  2. Convert that position into the position of your virtual document - for the ImageBox control we use the PointToImage method
  3. Perform your zoom and recalculate the document scroll size etc.
  4. Call the ScrollTo method, passing in the document position and mouse cursor position

And now you end up with something similar to this:

Mouse wheel zoom - works a lot smoother now.

There is one case where this does not work as expected - when you scroll in or out sufficiently to remove the scrollbars, or when moving from no-scrollbars to scrollbars. However, I think is fine given it works so well the rest of the time!

That's fine, but where's the ImageBox update?

Thanks to a generous donation from a visitor to the site, I recently sat down to work on the ImageBox control and resolve some of the issues - like the scrolling above. The next update has quite a lot of new functionality (better keyboard support, configurable zoom levels, flicker free scrolling and a handful of bug fixes to name a few of the changes) and will be posted presently. While the build is being finalized however, the above code will work fine in current builds of the ImageBox, if you adjust for the pixel offset the current PointToImage implementation uses.

Viewing all 559 articles
Browse latest View live