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

Boulder Dash Part 2: Collision Detection

$
0
0

In our previous post we introduced the Firefly and Butterfly sprites and their movement rules around a random map. Now we're going to update the project to include collision detection and a new player sprite.

A sample project showing sprites colliding with each other and moving on. The blue sprite will be destroyed if it comes in contact with any other sprite.

Refactoring AiTest

To start with though, we're going to do a little refactoring. The ButterflySprite and FireflySprite classes share pretty much the same movement code and will share exactly the same collision code, so we'll merge the common behaviour into a new abstract EnemySprite class which these will inherit from. We'll also add a protected constructor which will allow us to specify the differences between the inherited sprites and their movement rules.

protected EnemySprite(Direction preferredDirection, Direction fallbackDirection)
{this.PreferredDirection = preferredDirection;this.FallbackDirection = fallbackDirection;
}protected Direction FallbackDirection { get; set; }protected Direction PreferredDirection { get; set; }

With that done, we'll change FireflySprite and ButterflySprite to inherit from EnemySprite instead of Sprite, and update the constructors of these classes to call the protected constructor to supply the movement rule differences.

Finally, we'll remove the Move methods from the two sprite classes and instead let EnemySprite implement the movement code.

public ButterflySprite()  : base(Direction.Right, Direction.Left)public FireflySprite()  : base(Direction.Left, Direction.Right)

Although I won't go into this here, other refactoring we did was to move the Sprites collection from MainForm into Map. I also added an IsScenery method to the Map class which returns if a tile is considered scenery, for example a piece of solid earth, or a boulder which can't currently move.

A basic load map system was also added. You can still use the "Create Random Map" to generate a mostly empty canvas for the sprites to move around in (clicking with the left button will add a new firefly, with the right a butterfly) or you can load the predefined map.

Collision Detection

Now it's time to implement the actual collision detection. We'll do this by adding a new function to the base Sprite class that will check to see if a the location of any sprite matches a given location.

Note: This implementation assumes that only one sprite can occupy a tile at any one time, which is the case in Boulder Dash.

We're also using LINQ in this function for convenience. If you haven't yet upgraded to Visual Studio 2008/2010 you'll need to replace the call with a manual loop

publicbool IsCollision(Point location, out Sprite sprite)
{
  sprite = this.Map.Sprites.SingleOrDefault(s => s.Location == location);return sprite != null;
}

I choose to implement the function as a bool to allow it to be easily used in an if statement, but providing an out parameter to return the matching sprite (or null otherwise).

With that done, it's time to update our movement code to also perform the collision detection. The two conditions in the Move method which check if a tile is part of the scenery will be modified to call out new method.

if (!this.Map.IsScenery(tile) && !this.IsCollision(tile.Location, out collision))

With this change, sprites on the map are now aware of each other and when they bump into each other they will automatically turn away.

Collision Actions

Our example project now has collision detection in place for the enemy sprites. Being enemies of the player nothing happens when they bump into each other. If they bump into the player on the other hand...

Time to add a new sprite. The PlayerSprite will be a non functioning sprite masquerading as a player character.

class PlayerSprite : Sprite
{publicoverridevoid Move()
  {// Do nothing, this sprite doesn't automatically move
  }publicoverride Color Color
  {get { return Color.Aquamarine; }
  }
}

In our previous modification the Move method of our EnemySprite implementations we grab the sprite that we are colliding with, but we don't do anything with it. Time to change that.

We'll add a basic enum that will control what happens when a sprite hits another. For this demo, that will either be nothing, or "explode" killing both sprites.

enum CollisionAction
{
  None,
  Explode
}

We're also going to modify the base Sprite class with a new method:

publicabstract CollisionAction GetCollisionAction(Sprite collidedWith);

Sprite implementations will override this method and return a CollisionAction based on the sprite they collided with.

The implementation for our new Player class is quite straightforward:

publicoverride CollisionAction GetCollisionAction(Sprite collidedWith)
{return CollisionAction.Explode; // Player dies if it touches any other sprites
}

And the one for EnemySprite is almost as easy:

publicoverride CollisionAction GetCollisionAction(Sprite collidedWith)
{
  CollisionAction result;if (collidedWith is PlayerSprite)
    result = CollisionAction.Explode; // Kill playerelse
    result = CollisionAction.None; // Do nothingreturn result;
}

Now we have this, we'll update the Move method of our EnemySprite to take care of the action:

// if we collided with a sprite, lets execute the actionif (collision != null&& this.GetCollisionAction(collision)== CollisionAction.Explode)
{// kill both this sprite and the one we collided withthis.Map.Sprites.Remove(collision);this.Map.Sprites.Remove(this);
}

Note that if the Player could move as well then it too would need collision detection. However, as we only have one class capable of movement we'll add the code just to that for now.

Also note that we had to adjust the original NextMove method in MainForm otherwise it would crash when looping through the sprite list and a removal occurred.

for (int i = _map.Sprites.Count; i > 0; i--)
  _map.Sprites[i - 1].Move();

Sample Project

You can download an updated version of the sample project from the link below.

Downloads

All content Copyright © by Cyotek Ltd or its respective writers. Permission to reproduce news and web log entries and other RSS feed content in unmodified form without notice is granted provided they are not used to endorse or promote any products or opinions (other than what was expressed by the author) and without taking them out of context. Written permission from the copyright owner must be obtained for everything else.
Original URL of this content is http://www.cyotek.com/blog/boulder-dash-part-2-collision-detection?source=rss


Adding a horizontal scrollbar to a ComboBox using C#

$
0
0

In our WebCopy application we decided to update the User Agent configuration to allow selection from a predefined list of common agents, but still allow the user to enter their own custom agent if required.

Rather than use two separate fields, we choose to use a ComboBox in simple mode, which is both a textbox and a listbox in a single control. This mode seems somewhat out of fashion, I think the only place I see it used is in the Font common dialog, virtually unchanged since Windows 3.1.

The problem was immediately apparent however on firing up WebCopy and going to select a user agent - the agent strings can be very long, far longer than the width of the control.

Unfortunately however, the .NET ComboBox doesn't allow you to directly enable horizontal scrolling. So we'll do it the old fashioned way using the Windows API.

In order for a window to support horizontal scrolling, it needs to have the WS_HSCROLL style applied to it. And to setup the horizontal scrollbar, we need to call the SendMessage API with the CB_SETHORIZONTALEXTENT message.

A sample project showing a horizontal scrollbar attached to each display type of a ComboBox.

As usual, we'll be starting off by creating a new Component, which we'll inherit from ComboBox.

Traditionally, you would call GetWindowLong and SetWindowLong API's with the GWL_STYLE or GWL_EXSTYLE flags. However, we can more simply override the CreateParams property of our component and set the new style when the control is created.

protectedoverride CreateParams CreateParams
{get
  {
    CreateParams createParams;

    createParams = base.CreateParams;
    createParams.Style |= WS_HSCROLL;return createParams;
  }
}

With that done, we can now inform Windows of the size of the horizontal scroll area, and it will automatically add the scrollbar if required. To do this, I'll add two new methods to the component. The first will set the horizontal extent to a given value. The second will calculate the length of the longest piece of text in the control and then set the extent to match.

publicvoid SetHorizontalExtent()
{int maxWith;

  maxWith = 0;
  foreach (object item inthis.Items)
  {
    Size textSize;

    textSize = TextRenderer.MeasureText(item.ToString(), this.Font);if (textSize.Width > maxWith)
      maxWith = textSize.Width;
  }this.SetHorizontalExtent(maxWith);
}publicvoid SetHorizontalExtent(int width)
{
  SendMessage(this.Handle, CB_SETHORIZONTALEXTENT, new IntPtr(width), IntPtr.Zero);
}

The first overload of SetHorizontalExtent iterates through all the items in the control and uses the TextRenderer object to measure the size of the text. Once it has found the largest piece of text, it calls the second overload with the size.

The second overload does the actual work of notifying Windows using the SendMessage call, CB_SETHORIZONTALEXTENT message and the given width. SendMessage takes two configuration parameters per message, but CB_SETHORIZONTALEXTENT only requires one, and so we send 0 for the second.

The above function works with all display modes of the ComboBox.

For completeness, here are the API declarations we are using:

privateconstint WS_HSCROLL = 0x100000;privateconstint CB_SETHORIZONTALEXTENT = 0x015E;

[DllImport("user32.dll")]privatestaticextern IntPtr SendMessage(IntPtr hWnd, UInt32 msg, IntPtr wParam, IntPtr lParam);

As usual, a demonstration project is available from the link below.

Downloads

All content Copyright © by Cyotek Ltd or its respective writers. Permission to reproduce news and web log entries and other RSS feed content in unmodified form without notice is granted provided they are not used to endorse or promote any products or opinions (other than what was expressed by the author) and without taking them out of context. Written permission from the copyright owner must be obtained for everything else.
Original URL of this content is http://www.cyotek.com/blog/adding-a-horizontal-scrollbar-to-a-combobox-using-csharp?source=rss

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

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

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

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

The sample project in action.

Creating the component

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

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

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

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

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

Setting control styles

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

  • AllPaintingInWmPaint - We don't need a separate OnPaintBackground and OnPaint mechanism, OnPaint will do fine.
  • UserPaint - As we are doing entirely our own painting, we disable the base Control's painting.
  • OptimizedDoubleBuffer - Double buffering means the painting will occur in a memory buffer before being transferred to the screen, reducing flicker.
  • ResizeRedraw - Automatically redraw the component if it is resized.
  • Selectable - We disable this flag as we don't want the control to be receiving focus.
public ImageBox()
{
  InitializeComponent();this.SetStyle(ControlStyles.AllPaintingInWmPaint | ControlStyles.UserPaint | ControlStyles.OptimizedDoubleBuffer| ControlStyles.ResizeRedraw, true);this.SetStyle(ControlStyles.Selectable, false);this.UpdateStyles();this.BackColor = Color.White;this.TabStop = false;this.AutoSize = true;this.GridScale = ImageBoxGridScale.Small;this.ShowGrid = true;this.GridColor = Color.Gainsboro;this.GridColorAlternate = Color.White;this.GridCellSize = 8;this.BorderStyle = BorderStyle.FixedSingle;
}

Creating the background

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

protectedvirtual Bitmap CreateGridTileImage(int cellSize, Color firstColor, Color secondColor)
{
  Bitmap result;int width;int height;float scale;// rescale the cell sizeswitch (this.GridScale)
  {case ImageBoxGridScale.Medium:
      scale = 1.5F;break;case ImageBoxGridScale.Large:
      scale = 2;break;default:
      scale = 1;break;
  }

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

Painting the control

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

protectedoverridevoid OnPaint(PaintEventArgs e)
{if (_gridTile != null&& this.ShowGrid)
  {// draw the backgroundfor (int x = 0; x < this.ClientSize.Width; x += _gridTile.Size.Width)
    {for (int y = 0; y < this.ClientSize.Height; y += _gridTile.Size.Height)
        e.Graphics.DrawImageUnscaled(_gridTile, x, y);
    }
  }else
  {using (SolidBrush brush = new SolidBrush(this.BackColor))
      e.Graphics.FillRectangle(brush, this.ClientRectangle);
  }// draw the imageif (this.Image != null)
  {
    e.Graphics.DrawImageUnscaled(this.Image, new Point(this.Padding.Left + this.GetBorderOffset(), this.Padding.Top + this.GetBorderOffset()));
  }// draw the bordersswitch (this.BorderStyle)
  {case BorderStyle.FixedSingle:
      ControlPaint.DrawBorder(e.Graphics, this.ClientRectangle, this.ForeColor, ButtonBorderStyle.Solid);break;case BorderStyle.Fixed3D:
      ControlPaint.DrawBorder3D(e.Graphics, this.ClientRectangle, Border3DStyle.Sunken);break;
  }
}

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

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

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

Sample Project

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

Downloads

All content Copyright © by Cyotek Ltd or its respective writers. Permission to reproduce news and web log entries and other RSS feed content in unmodified form without notice is granted provided they are not used to endorse or promote any products or opinions (other than what was expressed by the author) and without taking them out of context. Written permission from the copyright owner must be obtained for everything else.
Original URL of this content is http://www.cyotek.com/blog/creating-a-scrollable-and-zoomable-image-viewer-in-csharp-part-1?source=rss

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

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

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

The ImageBox component, demonstrated in a sample application

Setting up auto scrolling

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

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

Using the above we can now offer full scrolling.

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

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

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

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

protectedvirtualvoid AdjustLayout()
{if (this.AutoSize)this.AdjustSize();elseif (this.AutoScroll)this.AdjustScrolling();
}protectedvirtualvoid AdjustScrolling()
{if (this.AutoScroll && this.Image != null)this.AutoScrollMinSize = this.Image.Size;
}

Reacting to scroll changes

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

protectedoverridevoid OnScroll(ScrollEventArgs se)
{this.Invalidate();base.OnScroll(se);
}

Painting adjustments

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

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

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

protectedoverridevoid OnPaint(PaintEventArgs e)
{int borderOffset;
  Rectangle innerRectangle;

  borderOffset = this.GetBorderOffset();if (borderOffset != 0)
  {// draw the bordersswitch (this.BorderStyle)
    {case BorderStyle.FixedSingle:
        ControlPaint.DrawBorder(e.Graphics, this.ClientRectangle, this.ForeColor, ButtonBorderStyle.Solid);break;case BorderStyle.Fixed3D:
        ControlPaint.DrawBorder3D(e.Graphics, this.ClientRectangle, Border3DStyle.Sunken);break;
    }// clip the background so we don't overwrite the border
    innerRectangle = Rectangle.Inflate(this.ClientRectangle, -borderOffset, -borderOffset);
    e.Graphics.SetClip(innerRectangle);
  }else
    innerRectangle = this.ClientRectangle;// draw the backgroundif (_texture != null&& this.ShowGrid)
    e.Graphics.FillRectangle(_texture, innerRectangle);else
  {using (SolidBrush brush = new SolidBrush(this.BackColor))
      e.Graphics.FillRectangle(brush, innerRectangle);
  }// draw the imageif (this.Image != null)
  {int left;int top;

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

    e.Graphics.DrawImageUnscaled(this.Image, new Point(left, top));
  }// reset the clippingif (borderOffset != 0)
    e.Graphics.ResetClip();
}

Sample Project

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

Downloads

All content Copyright © by Cyotek Ltd or its respective writers. Permission to reproduce news and web log entries and other RSS feed content in unmodified form without notice is granted provided they are not used to endorse or promote any products or opinions (other than what was expressed by the author) and without taking them out of context. Written permission from the copyright owner must be obtained for everything else.
Original URL of this content is http://www.cyotek.com/blog/creating-a-scrollable-and-zoomable-image-viewer-in-csharp-part-2?source=rss

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

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

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

The ImageBox component, demonstrated in a sample application

Design support

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

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

Mouse Panning

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

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

protectedoverridevoid OnMouseMove(MouseEventArgs e)
{base.OnMouseMove(e);if (e.Button == MouseButtons.Left && this.AutoPan && this.Image != null)
  {if (!this.IsPanning)
    {
      _startMousePosition = e.Location;this.IsPanning = true;
    }if (this.IsPanning)
    {int x;int y;
      Point position;if (!this.InvertMouse)
      {
        x = -_startScrollPosition.X + (_startMousePosition.X - e.Location.X);
        y = -_startScrollPosition.Y + (_startMousePosition.Y - e.Location.Y);
      }else
      {
        x = -(_startScrollPosition.X + (_startMousePosition.X - e.Location.X));
        y = -(_startScrollPosition.Y + (_startMousePosition.Y - e.Location.Y));
      }

      position = new Point(x, y);this.UpdateScrollPosition(position);
    }
  }
}protectedoverridevoid OnMouseUp(MouseEventArgs e)
{base.OnMouseUp(e);if (this.IsPanning)this.IsPanning = false;
}protectedvirtualvoid UpdateScrollPosition(Point position)
{this.AutoScrollPosition = position;this.Invalidate();this.OnScroll(new ScrollEventArgs(ScrollEventType.ThumbPosition, 0));
}

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

[DefaultValue(false), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden), Browsable(false)]publicbool IsPanning
{get { return _isPanning; }protectedset
  {if (_isPanning != value)
    {
      _isPanning = value;
      _startScrollPosition = this.AutoScrollPosition;if (value)
      {this.Cursor = Cursors.SizeAll;this.OnPanStart(EventArgs.Empty);
      }else
      {this.Cursor = Cursors.Default;this.OnPanEnd(EventArgs.Empty);
      }
    }
  }
}

Keyboard Scrolling

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

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

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

protectedoverridebool IsInputKey(Keys keyData)
{bool result;if ((keyData & Keys.Right) == Keys.Right | (keyData & Keys.Left) == Keys.Left | (keyData & Keys.Up) == Keys.Up | (keyData & Keys.Down) == Keys.Down)
    result = true;else
    result = base.IsInputKey(keyData);return result;
}protectedoverridevoid OnKeyDown(KeyEventArgs e)
{base.OnKeyDown(e);switch (e.KeyCode)
  {case Keys.Left:this.AdjustScroll(-(e.Modifiers == Keys.None ? this.HorizontalScroll.SmallChange : this.HorizontalScroll.LargeChange), 0);break;case Keys.Right:this.AdjustScroll(e.Modifiers == Keys.None ? this.HorizontalScroll.SmallChange : this.HorizontalScroll.LargeChange, 0);break;case Keys.Up:this.AdjustScroll(0, -(e.Modifiers == Keys.None ? this.VerticalScroll.SmallChange : this.VerticalScroll.LargeChange));break;case Keys.Down:this.AdjustScroll(0, e.Modifiers == Keys.None ? this.VerticalScroll.SmallChange : this.VerticalScroll.LargeChange);break;
  }
}protectedvirtualvoid AdjustScroll(int x, int y)
{
  Point scrollPosition;

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

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

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

Sample Project

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

Downloads

All content Copyright © by Cyotek Ltd or its respective writers. Permission to reproduce news and web log entries and other RSS feed content in unmodified form without notice is granted provided they are not used to endorse or promote any products or opinions (other than what was expressed by the author) and without taking them out of context. Written permission from the copyright owner must be obtained for everything else.
Original URL of this content is http://www.cyotek.com/blog/creating-a-scrollable-and-zoomable-image-viewer-in-csharp-part-3?source=rss

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

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

In the conclusion to our series on building a scrollable and zoomable image viewer, we'll add support for zooming, auto centering, size to fit and some display optimizations and enhancements.

The ImageBox sample application showing a zoomed in image.

The ImageBox sample application showing a zoomed out image, with auto centering and the transparency grid only displayed behind the image.

Getting Started

Unlike parts 2 and 3, we're actually adding quite a lot of new functionality, some of it more complicated than others.

First, we're going to remove the ShowGrid property. This originally was a simple on/off flag, but we want more control this time.

We've also got a number of new properties and backing events to add:

  • AutoCenter - controls if the image is automatically centered in the display area if the image isn't scrolled.
  • SizeToFit - if this property is set, the image will automatically zoom to the maximum size for displaying the entire image.
  • GridDisplayMode - this property, which replaces ShowGrid will determine how the background grid is to be drawn.
  • InterpolationMode - determines how the zoomed image will be rendered.
  • Zoom - allows you to specify the zoom level.
  • ZoomIncrement - specifies how much the zoom is increased or decreased using the scroll wheel.
  • ZoomFactor - this protected property returns the current zoom as used internally for scalling.
  • ScaledImageWidth and ScaledImageHeight - these protected properties return the size of the image adjusted for the current zoom.

Usually the properties are simple assignments, which compare the values before assignment and raise an event. The zoom property is slightly different as it will ensure that the new value fits within a given range before setting it.

privatestaticreadonlyint MinZoom = 10;privatestaticreadonlyint MaxZoom = 3500;

[DefaultValue(100), Category("Appearance")]publicint Zoom
{get { return _zoom; }set
  {if (value < ImageBox.MinZoom)
      value = ImageBox.MinZoom;elseif (value > ImageBox.MaxZoom)
      value = ImageBox.MaxZoom;if (_zoom != value)
    {
      _zoom = value;this.OnZoomChanged(EventArgs.Empty);
    }
  }
}

Using the MinZoom and MaxZoom constants we are specifying a minimum value of 10% and a maximum of 3500%. The values you are assign are more or less down to your own personal preferences - I don't have any indications of what a "best" maximum value would be.

Setting the SizeToFit property should disable the AutoPan property and vice versa.

Layout Updates

Several parts of the component work from the image size, however as these now need to account for any zoom level, all such calls now use the ScaledImageWidth and ScaledImageHeight properties.

protectedvirtualint ScaledImageHeight
{ get { returnthis.Image != null ? (int)(this.Image.Size.Height * this.ZoomFactor) : 0; } }protectedvirtualint ScaledImageWidth
{ get { returnthis.Image != null ? (int)(this.Image.Size.Width * this.ZoomFactor) : 0; } }protectedvirtualdouble ZoomFactor
{ get { return (double)this.Zoom / 100; } }

The AdjustLayout method which determines the appropriate course of action when certain properties are changed has been updated to support the size to fit functionality by calling the new ZoomToFit method.

protectedvirtualvoid AdjustLayout()
{if (this.AutoSize)this.AdjustSize();elseif (this.SizeToFit)this.ZoomToFit();elseif (this.AutoScroll)this.AdjustViewPort();this.Invalidate();
}publicvirtualvoid ZoomToFit()
{if (this.Image != null)
  {
    Rectangle innerRectangle;double zoom;double aspectRatio;this.AutoScrollMinSize = Size.Empty;

    innerRectangle = this.GetInsideViewPort(true);if (this.Image.Width > this.Image.Height)
    {
      aspectRatio = ((double)innerRectangle.Width) / ((double)this.Image.Width);
      zoom = aspectRatio * 100.0;if (innerRectangle.Height < ((this.Image.Height * zoom) / 100.0))
      {
        aspectRatio = ((double)innerRectangle.Height) / ((double)this.Image.Height);
        zoom = aspectRatio * 100.0;
      }
    }else
    {
      aspectRatio = ((double)innerRectangle.Height) / ((double)this.Image.Height);
      zoom = aspectRatio * 100.0;if (innerRectangle.Width < ((this.Image.Width * zoom) / 100.0))
      {
        aspectRatio = ((double)innerRectangle.Width) / ((double)this.Image.Width);
        zoom = aspectRatio * 100.0;
      }
    }this.Zoom = (int)Math.Round(Math.Floor(zoom));
  }
}

Due to the additional complexity in positioning and sizing, we're also adding functions to return the different regions in use by the control.

  • GetImageViewPort - returns a rectangle representing the size of the drawn image.
  • GetInsideViewPort - returns a rectangle representing the client area of the control, offset by the current border style, and optionally padding.
  • GetSourceImageRegion - returns a rectangle representing the area of the source image that will be drawn onto the control.

The sample project has been updated to be able to display the results of the GetImageViewPort and GetSourceImageRegion functions.

publicvirtual Rectangle GetImageViewPort()
{
  Rectangle viewPort;if (this.Image != null)
  {
    Rectangle innerRectangle;
    Point offset;

    innerRectangle = this.GetInsideViewPort();if (this.AutoCenter)
    {int x;int y;

      x = !this.HScroll ? (innerRectangle.Width - (this.ScaledImageWidth + this.Padding.Horizontal)) / 2 : 0;
      y = !this.VScroll ? (innerRectangle.Height - (this.ScaledImageHeight + this.Padding.Vertical)) / 2 : 0;

      offset = new Point(x, y);
    }else
      offset = Point.Empty;

    viewPort = new Rectangle(offset.X + innerRectangle.Left + this.Padding.Left, offset.Y + innerRectangle.Top + this.Padding.Top, innerRectangle.Width - (this.Padding.Horizontal + (offset.X * 2)), innerRectangle.Height - (this.Padding.Vertical + (offset.Y * 2)));
  }else
    viewPort = Rectangle.Empty;return viewPort;
}public Rectangle GetInsideViewPort()
{returnthis.GetInsideViewPort(false);
}publicvirtual Rectangle GetInsideViewPort(bool includePadding)
{int left;int top;int width;int height;int borderOffset;

  borderOffset = this.GetBorderOffset();
  left = borderOffset;
  top = borderOffset;
  width = this.ClientSize.Width - (borderOffset * 2);
  height = this.ClientSize.Height - (borderOffset * 2);if (includePadding)
  {
    left += this.Padding.Left;
    top += this.Padding.Top;
    width -= this.Padding.Horizontal;
    height -= this.Padding.Vertical;
  }returnnew Rectangle(left, top, width, height);
}publicvirtual Rectangle GetSourceImageRegion()
{int sourceLeft;int sourceTop;int sourceWidth;int sourceHeight;
  Rectangle viewPort;
  Rectangle region;if (this.Image != null)
  {
    viewPort = this.GetImageViewPort();
    sourceLeft = (int)(-this.AutoScrollPosition.X / this.ZoomFactor);
    sourceTop = (int)(-this.AutoScrollPosition.Y / this.ZoomFactor);
    sourceWidth = (int)(viewPort.Width / this.ZoomFactor);
    sourceHeight = (int)(viewPort.Height / this.ZoomFactor);

    region = new Rectangle(sourceLeft, sourceTop, sourceWidth, sourceHeight);
  }else
    region = Rectangle.Empty;return region;
}

Drawing the control

As with the previous versions, the control is drawn by overriding OnPaint, this time we are not using clip regions or drawing the entire image even if only a portion of it is visible.

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

Depending on the value of the GridDisplayMode property, the background tile grid will either not be displayed, will be displayed to fill the client area of the control, or new for this update, to only fill the area behind the image. The remainder of the control is filled with the background color.

  Rectangle innerRectangle;

  innerRectangle = this.GetInsideViewPort();// draw the backgroundusing (SolidBrush brush = new SolidBrush(this.BackColor))
    e.Graphics.FillRectangle(brush, innerRectangle);if (_texture != null&& this.GridDisplayMode != ImageBoxGridDisplayMode.None)
  {switch (this.GridDisplayMode)
    {case ImageBoxGridDisplayMode.Image:
        Rectangle fillRectangle;

        fillRectangle = this.GetImageViewPort();
        e.Graphics.FillRectangle(_texture, fillRectangle);if (!fillRectangle.Equals(innerRectangle))
        {
          fillRectangle.Inflate(1, 1);
          ControlPaint.DrawBorder(e.Graphics, fillRectangle, this.ForeColor, ButtonBorderStyle.Solid);
        }break;case ImageBoxGridDisplayMode.Client:
        e.Graphics.FillRectangle(_texture, innerRectangle);break;
    }
  }

Previous versions of the control drew the entire image using the DrawImageUnscaled method of the Graphics object. In this final version, we're going to be a little more intelligent and only draw the visible area, removing the need for the previous clip region. The InterpolationMode is used to determine how the image is drawn when it is zoomed in or out.

// draw the image
  g.InterpolationMode = this.InterpolationMode;
  g.DrawImage(this.Image, this.GetImageViewPort(), this.GetSourceImageRegion(), GraphicsUnit.Pixel);

Zooming Support

With the control now all set up and fully supporting zoom, it's time to allow the end user to be able to change the zoom.

The first step is to disable the ability to double click the control, by modifying the control styles in the constructor.

this.SetStyle(ControlStyles.StandardDoubleClick, false);

We're going to allow the zoom to be changed two ways - by either scrolling the mouse wheel, or left/right clicking the control.

By overriding OnMouseWheel, we can be notified when the user spins the wheel, and in which direction. We then adjust the zoom using the value of the ZoomIncrement property. If a modifier key such as Shift or Control is pressed, then we'll modify the zoom by five times the increment.

protectedoverridevoid OnMouseWheel(MouseEventArgs e)
{if (!this.SizeToFit)
  {int increment;if (Control.ModifierKeys == Keys.None)
      increment = this.ZoomIncrement;else
      increment = this.ZoomIncrement * 5;if (e.Delta < 0)
      increment = -increment;this.Zoom += increment;
  }
}

Normally, whenever we override a method, we always call it's base implementation. However, in this case we will not; the ScrollbableControl that we inherit from uses the mouse wheel to scroll the viewport and there doesn't seem to be a way to disable this undesirable behaviour.

As we also want to allow the user to be able to click the control with the left mouse button to zoom in, and either the right mouse button or left button holding a modifier key to zoom out, we'll also override OnMouseClick.

protectedoverridevoid OnMouseClick(MouseEventArgs e)
{if (!this.IsPanning && !this.SizeToFit)
  {if (e.Button == MouseButtons.Left && Control.ModifierKeys == Keys.None)
    {if (this.Zoom >= 100)this.Zoom = (int)Math.Round((double)(this.Zoom + 100) / 100) * 100;elseif (this.Zoom >= 75)this.Zoom = 100;elsethis.Zoom = (int)(this.Zoom / 0.75F);
    }elseif (e.Button == MouseButtons.Right || (e.Button == MouseButtons.Left && Control.ModifierKeys != Keys.None))
    {if (this.Zoom > 100 && this.Zoom <= 125)this.Zoom = 100;elseif (this.Zoom > 100)this.Zoom = (int)Math.Round((double)(this.Zoom - 100) / 100) * 100;elsethis.Zoom = (int)(this.Zoom * 0.75F);
    }
  }base.OnMouseClick(e);
}

Unlike with the mouse wheel and it's fixed increment, we want to use a different approach with clicking. If zooming out and the percentage is more than 100, then the zoom level will be set to the current zoom level + 100, but rounded to the nearest 100, and the same in reserve for zooming in.

If the current zoom is less than 100, then the new value will +- 75% of the current zoom, or reset to 100 if the new value falls between 75 and 125.

This results in a nicer zoom experience then just using a fixed value.

Sample Project

You can download the final sample project from the link below.

What's next?

One of the really annoying issues with this control that has plagued me during writing this series is scrolling the component. During scrolling there is an annoying flicker as the original contents are moved, then the new contents are drawn. At present I don't have a solution for this, I've tried overriding various WM_* messages but without success. A future update to this component will either fix this issue, or do it's own scrollbar support without inheriting from ScrollableControl, although I'd like to avoid this latter solution.

If anyone knows of a solution please let us know!

Another enhancement would be intelligent use of the interpolation mode. Currently the control uses a fixed value, but some values are better when zoomed in, and some better when zoomed out. The ability for the control to automatically select the most appropriate mode would be useful.

Downloads

All content Copyright © by Cyotek Ltd or its respective writers. Permission to reproduce news and web log entries and other RSS feed content in unmodified form without notice is granted provided they are not used to endorse or promote any products or opinions (other than what was expressed by the author) and without taking them out of context. Written permission from the copyright owner must be obtained for everything else.
Original URL of this content is http://www.cyotek.com/blog/creating-a-scrollable-and-zoomable-image-viewer-in-csharp-part-4?source=rss

Creating a trackback handler using C#

$
0
0

Cyotek.com runs on its own custom CMS/blog engine developed in ASP.NET MVC 1.0, which has a number of advantages and disadvantages. One of these disadvantages is no automatic support for some common blog features such as trackbacks and pingbacks.

This article will describe how to create a trackback handler for use with MVC and the more traditional webforms.

What is a trackback?

A trackback is a way to be notified when a website links to a resource on your own site. Some blogging software supports automatic linking, so if a post on that site links to another, when the post is submitted, it will automatically detect the link and attempt to send a trackback to the original author. If successful, a link is generally created from the original author to the new post, thus building a web of interconnected resources (in theory). You can learn a little more about trackbacks from Wikiepedia.

The full trackback specification can be viewed at the SixApart website

A trackback handler in C#

Unlike pingbacks (which we'll address in a future article), trackbacks use standard HTTP requests and so are extremely easy to implement.

Available for download at the end of this article is a sample library which you can use to implement your trackbacks.

As a trackback is comprised of several pieces of information which we'll be passing about, we'll start by defining a structure to hold this information.

publicstruct TrackbackInfo
{publicstring BlogName { get; set; }publicstring Excerpt { get; set; }publicstring Id { get; set; }publicstring Title { get; set; }public Uri Uri { get; set; }
}

The properties of this structure mirror the required information from the trackback specification.

Next, we'll define an enum for the different result codes you can return. The specification states 0 for success and 1 for error, but I'm uncertain if you can extend this, ie is any non-zero is classed as an error. We'll play it safe and just use a single error code.

publicenum TrackbackErrorCode
{
  Success,
  Error
}

I'd considered two ways of implementing this, the first being an abstract class containing methods which must be implemented in order to provide the functionality for saving a trackback into your chosen data source, or using delegates. In order to make it a simple as possible to use, I've went with the latter. Therefore, we need two delegates, one which will resolve the "permalink" for the given ID, and another to actually save the trackback.

publicdelegate Uri GetTrackbackUrlDelegate(TrackbackInfo trackback);publicdelegatevoid SaveTrackbackDelegate(TrackbackInfo trackback);

Implementing the handler

We've created a static class named TrackbackHandler which contains all the functionality we'll need. We expose a single public method, GetTrackback, which will return the XML block required to notify the sender of the result of the request.

publicstaticstring GetTrackback(NameValueCollection form, SaveTrackbackDelegate saveTrackbackDelegate, GetTrackbackUrlDelegate getTrackbackUrlDelegate)
{string url;if (form == null)thrownew ArgumentNullException("form");if (saveTrackbackDelegate == null)thrownew ArgumentNullException("saveTrackbackDelegate");if (getTrackbackUrlDelegate == null)thrownew ArgumentNullException("getTrackbackUrlDelegate");

  url = form["url"];if (!string.IsNullOrEmpty(url) && url.Contains(","))
    url = url.Split(',')[0];return TrackbackHandler.GetTrackback(saveTrackbackDelegate, getTrackbackUrlDelegate, form["id"], url, form["title"], form["excerpt"], form["blog_name"]);
}

This function accepts the following arguments:

  • A NameValueCollection holding the submitted trackback data - supporting both the MVC FormCollection or Request.Form for ASP.NET.
  • An implementation of the SaveTrackbackDelegate delgate for saving the trackback to your choosen data store.
  • An implementation of the GetTrackbackUrlDelegate for resolving a permalink URL of the given ID.

Assuming none of these are null, the method then calls a private overload, explicitly specifying the individual items of data.

privatestaticstring GetTrackback(SaveTrackbackDelegate saveTrackbackDelegate, GetTrackbackUrlDelegate getTrackbackUrlDelegate, string id, string url, string title, string excerpt, string blogName)
{string result;try
  {
    HttpRequest request;

    request = HttpContext.Current.Request;

    if (string.IsNullOrEmpty(id))
      result = GetTrackbackResponse(TrackbackErrorCode.Error, "The entry ID is missing");elseif (request.HttpMethod != "POST")
      result = GetTrackbackResponse(TrackbackErrorCode.Error, "An invalid request was made.");elseif (string.IsNullOrEmpty(url))
      result = TrackbackHandler.GetTrackbackResponse(TrackbackErrorCode.Error, "Trackback URI not specified.");

First, we validate that the request is being made via a POST and not any other HTTP request, and that both the entry ID and the URL of the sender are specified.

else
    {
      TrackbackInfo trackbackInfo;string trackbackTitle;
      Uri targetUri;

      trackbackInfo = new TrackbackInfo()
      {
        Id = id,
        Title = title,
        BlogName = blogName,
        Excerpt = excerpt,
        Uri = new Uri(url)
      };

      targetUri = getTrackbackUrlDelegate.Invoke(trackbackInfo);

If everything is fine, we then construct our TrackbackInfo object for passing to our delegates, and then try and get the permalink for the trackback ID.

if (targetUri == null)
        result = GetTrackbackResponse(TrackbackErrorCode.Error, "The entry ID could not be matched.");elseif (!TrackbackHandler.CheckSourceLinkExists(targetUri, trackbackInfo.Uri, out trackbackTitle))
        result = GetTrackbackResponse(TrackbackErrorCode.Error, string.Format("Sorry couldn't find a link for \"{0}\" in \"{1}\"", targetUri.ToString(), trackbackInfo.Uri.ToString()));

If we don't have a URL, we return an error code to the sender.

If we do have a URL another method, CheckSourceLinkExists is called. This method will download the HTML of the caller and attempt to verify if the senders page does in fact contain a link matching the permalink. If it doesn't, then we'll abort here.

If the method is successful and a link is detected, the method will return the title of the senders HTML page as an out parameter. This will be used if the trackback information didn't include a blog name (as this is an optional field).

else
      {if (string.IsNullOrEmpty(blogName))
          trackbackInfo.BlogName = trackbackTitle;

        saveTrackbackDelegate.Invoke(trackbackInfo);

        result = TrackbackHandler.GetTrackbackResponse(TrackbackErrorCode.Success, string.Empty);
      }
    }
  }catch (Exception ex)
  {//handle the error.
    result = TrackbackHandler.GetTrackbackResponse(TrackbackErrorCode.Error, ex.Message);
  }return result;
}

Finally, if everything went to plan, we save the trackback to our data store, and return a success code. In the event of any part of this process failing, then we return an error result.

In this implementation, we won't link to the senders site unless they have already linked to us. We do this by downloading the HTML of the senders site and checking to see if our link is present.

privatestaticbool CheckSourceLinkExists(Uri lookingFor, Uri lookingIn, outstring pageTitle)
{bool result;

  pageTitle = null;try
  {string html;

    html = GetPageHtml(lookingIn);

    if (string.IsNullOrEmpty(html.Trim()) | html.IndexOf(lookingFor.ToString(), StringComparison.InvariantCultureIgnoreCase) < 0)
      result = false;else
    {
      HtmlDocument document;

      document = new HtmlDocument();
      document.LoadHtml(html);
      pageTitle = document.GetDocumentTitle();

      result = true;
    }
  }catch
  {
    result = false;
  }return result;
}privatestaticstring GetPageHtml(Uri uri)
{
  WebRequest request;
  HttpWebResponse response;string encodingName;
  Encoding encoding;string result;

  request = WebRequest.Create(uri);
  response = (HttpWebResponse)request.GetResponse();

  encodingName = response.ContentEncoding.Trim();
  if (string.IsNullOrEmpty(encodingName))
    encodingName = "utf-8";
  encoding = Encoding.GetEncoding(encodingName);using (Stream stream = response.GetResponseStream())
  {using (StreamReader reader = new StreamReader(stream, encoding))
      result = reader.ReadToEnd();
  }return result;
}privatestaticstring GetDocumentTitle(this HtmlDocument document)
{
  HtmlNode titleNode;string title;

  titleNode = document.DocumentNode.SelectSingleNode("//head/title");if (titleNode != null)
    title = titleNode.InnerText;else
    title = string.Empty;

  title = title.Replace("\n", "");
  title = title.Replace("\r", "");while (title.Contains("  "))
    title = title.Replace("  ", " ");return title.Trim();
}

The function GetDocumentTitle uses the Html Agility Pack to parse the HTML looking for the title tag. As the CheckSourceLinkExists function is only checking to see if the link exists somewhere inside the HTML you may wish to update this to ensure that the link is actually within an anchor tag - the Html Agility Pack makes this extremely easy.

Returning a response

In several places, the GetTrackback method calls GetTrackbackResponse. This helper function returns a block of XML which describes the result of the operation.

privatestaticstring GetTrackbackResponse(TrackbackErrorCode errorCode, string errorText)
{
  StringBuilder builder;

  builder = new StringBuilder();using (StringWriter writer = new StringWriter(builder))
  {
    XmlWriterSettings settings;
    XmlWriter xmlWriter;

    settings = new XmlWriterSettings();
    settings.Indent = true;
    settings.Encoding = Encoding.UTF8;

    xmlWriter = XmlWriter.Create(writer, settings);

    xmlWriter.WriteStartDocument(true);
    xmlWriter.WriteStartElement("response");
    xmlWriter.WriteElementString("response", ((int)errorCode).ToString());if (!string.IsNullOrEmpty(errorText))
      xmlWriter.WriteElementString("message", errorText);
    xmlWriter.WriteEndElement();
    xmlWriter.WriteEndDocument();
    xmlWriter.Close();
  }return builder.ToString();
}

Implementing an MVC Action for handling trackbacks

In order to use the handler from MVC, define a new action which returns a ContentResult. It should only be callable from a POST, and ideally it shouldn't validate input. Even if you don't want HTML present in your trackbacks, you should strip any HTML yourself - if you have ASP.NET validation enabled and an attempt is made to post data containing HTML, then ASP.NET will return the yellow screen of death HTML to the sender, not the nice block of XML it was expecting.

Simply return a new ContentResult containing the result of the GetTrackback method and a mime type of text/xml, as shown below.

[AcceptVerbs(HttpVerbs.Post)]
[ValidateInput(false)]public ContentResult Trackback(FormCollection form)
{string xml;// get the ID of the article to link to from the URL query stringif (string.IsNullOrEmpty(form["id"]))
    form.Add("id", Request.QueryString["id"]);// get the response from the trackback handler
  xml = TrackbackHandler.GetTrackback(form, this.SaveTrackbackComment, this.GetArticleUrl);returnthis.Content(xml, "text/xml");
}

In this case, I'm also checking the query string for the ID of the article to link to as we use a single trackback action to handle all resources. If your trackback submission URL is unique for resource supporting trackbacks, then you wouldn't need to do this.

The implementations of your two delegates will vary depending on how your own website is structured and how it stores data. As an example I have included the ones used here at Cyotek.com (Entity Framework on SQL Server 2005 using a repository pattern):

private Uri GetArticleUrl(TrackbackInfo trackback)
{
  Article article;int articleId;
  Uri result;

  Int32.TryParse(trackback.Id, out articleId);

  article = this.ArticleService.GetItem(articleId);if (article != null)
    result = new Uri(Url.Action("display", "article", new { id = article.Name }, "http"));else
    result = null;return result;
}privatevoid SaveTrackbackComment(TrackbackInfo trackback)
{try
  {
    Comment comment;
    Article article;
    StringBuilder body;string blogName;

    article = this.ArticleService.GetItem(Convert.ToInt32(trackback.Id));

    blogName = !string.IsNullOrEmpty(trackback.BlogName) ? trackback.BlogName : trackback.Uri.AbsolutePath;

    body = new StringBuilder();
    body.AppendFormat("[b]{0}[/b]\n", trackback.Title);
    body.Append(trackback.Excerpt);
    body.AppendFormat(" - Trackback from {0}", blogName);

    comment = new Comment();
    comment.Article = article;
    comment.AuthorName = blogName;
    comment.AuthorUrl = trackback.Uri.ToString();
    comment.DateCreated = DateTime.Now;
    comment.Body = body.ToString();
    comment.IsPublished = true;
    comment.AuthorEmail = string.Empty;
    comment.AuthorUserName = null;this.CommentService.CreateItem(comment);

    ModelHelpers.SendCommentEmail(this, article, comment, this.Url);
  }catch (System.Exception ex)
  {
    CyotekApplication.LogException(ex);throw;
  }
}

Implementing an ASP.NET Webforms trackback handler

Using this library from ASP.NET webforms is almost as straightforward. You could, as in the example below, create a normal page containing no HTML such as trackback.aspx which will omit the XML when called.

Ideally however, you would probably want to implement this as a HTTP Handler, although this is beyond the scope of this article.

using System;using System.Text;using Cyotek.Web.Trackback;publicpartialclass TrackbackHandlerPage : System.Web.UI.Page
{protectedoverridevoid OnInit(EventArgs e)
  {base.OnInit(e);

    Response.ContentEncoding = Encoding.UTF8;
    Response.ContentType = "text/xml";

    Response.Clear();
    Response.Write(TrackbackHandler.GetTrackback(Request.Form, this.SaveTrackback, this.GetTrackbackUrl));
  }private Uri GetTrackbackUrl(TrackbackInfo trackbackInfo)
  {thrownew NotImplementedException();
  }privatevoid SaveTrackback(TrackbackInfo trackbackInfo)
  {thrownew NotImplementedException();
  }
}

Providing the trackback URL

Of course, having a trackback handler is of no use if third party sites can't find it! For sites to discover your trackback URL's, you need to embed a block of HTML inside your blog articles containing a link to your trackback handler. This URL should be unique for each article. For cyotek.com, we append the ID of the article as part of the query string of the URL, then extract this in the controller action, but this isn't the only way to do it - choose whatever suits the needs of your site.

The following shows the auto discovery information for this URL:

<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:trackback="http://madskills.com/public/xml/rss/module/trackback/"><rdf:Description rdf:about="http://cyotek.com/article/display/creating-a-trackback-handler-using-csharp" dc:identifier="http://cyotek.com/article/display/creating-a-trackback-handler-using-csharp" dc:title="Creating a trackback handler using C#" trackback:ping="http://cyotek.com/trackback?id=21"></rdf:Description></rdf:RDF>

It includes the trackback URL (with article ID 21) and the title of the article, plus the permalink.

Next steps

Cyotek.com doesn't get a huge amount of traffic, and so this library has not been extensively tested. It has worked so far, but I can't guarantee it to be bug free!

Possible enhancements would be to add some form of blacklisting, so if you were getting spam requests, you could more easily disable these. Also the link checking could be made more robust by ensure its within a valid anchor, although there's only so much you can do.

I hope you find this library useful, the download link is below. As mentioned, this library uses the Html Agility Pack for parsing HTML, however you can replace this if required with your own custom solution.

Downloads

All content Copyright © by Cyotek Ltd or its respective writers. Permission to reproduce news and web log entries and other RSS feed content in unmodified form without notice is granted provided they are not used to endorse or promote any products or opinions (other than what was expressed by the author) and without taking them out of context. Written permission from the copyright owner must be obtained for everything else.
Original URL of this content is http://www.cyotek.com/blog/creating-a-trackback-handler-using-csharp?source=rss

Comparing the properties of two objects via Reflection and C#

$
0
0

As part of the refactoring I was doing to the load code for crawler projects I needed a way of verifying that new code was loading data correctly. As it would be extremely time consuming to manually compare the objects, I used Reflection to compare the different objects and their properties. This article briefly describes the process and provides a complete helper function you can use in your own projects.

This code is loosely based on a stackoverflow question, but I have heavily modified and expanded the original concept.

Obtaining a list of properties

The ability to analyze assemblies and their component pieces is directly built into the .NET Framework, and something I really appreciate - I remember the nightmares of trying to work with COM type libraries in Visual Basic many years ago!

The Type class represents a type declaration in your project, such as a class or enumeration. You can either use the GetType method of any object to get its underlying type, or use the typeof keyword to access a type from its type name.

Type typeA;
Type typeB;int value;

value = 1;

typeA = value.GetType();
typeB = typeof(int);

Once you have a type, you can call the GetProperties method to return a list of PropertyInfo objects representing the available properties of the type. Several methods, including GetProperties, accept an argument of BindingFlags, these flags allow you to define the type of information return, such as public members or instance members.

In this case, I want all public instance members which can be read from and which are not included in a custom ignore list.

foreach (PropertyInfo propertyInfo in objectType.GetProperties(BindingFlags.Public | BindingFlags.Instance).Where(p => p.CanRead && !ignoreList.Contains(p.Name)))
{
}

Retrieving the value of a property

The PropertyInfo class has a GetValue method can be used to read the value of a property. Its most basic usage is to pass in the instance object (or null if you want to read a static property) and any index parameters (or null if no index parameters are supported).

object valueA;object valueB;

valueA = propertyInfo.GetValue(objectA, null);
valueB = propertyInfo.GetValue(objectB, null);

The sample function described in this article doesn't currently support indexed properties.

Determining if a property can be directly compared

Some properties are simple types, such as an int or a string and are very easy to compare. What happens if a property returns some other object such as a collection of strings, or a complex class?

In this case, I try and see if the type supports IComparable by calling the IsAssignableFrom method. You need to call this from the type you would like to create, passing in the source type.

returntypeof(IComparable).IsAssignableFrom(type)

I also check the IsPrimitive and IsValueType properties of the source type, although this is possibly redundant as all the base types I've checked so far all support IComparable.

Directly comparing values

Assuming that I can directly compare a value, first I check if one of the values is null - if one value is null and one false, I immediately return a mismatch.

Otherwise, if IComparable is available, then I obtain an instance of it from the first value and call its CompareTo method, passing in the second value.

If IComparable is not supported, then I fallback to **object.Equals.

bool result;
IComparable selfValueComparer;

selfValueComparer = valueA as IComparable;if (valueA == null&& valueB != null || valueA != null&& valueB == null)
  result = false; // one of the values is nullelseif (selfValueComparer != null&& selfValueComparer.CompareTo(valueB) != 0)
  result = false; // the comparison using IComparable failedelseif (!object.Equals(valueA, valueB))
  result = false; // the comparison using Equals failedelse
  result = true; // matchreturn result;

Comparing objects

If the values could not be directly compared, and do not implement IEnumerable (as described in the next section) then I assume the properties are objects and call the compare objects function again on the properties.

This works nicely, but has one critical flaw - if you have a child object which has a property reference to a parent item, then the function will get stuck in a recursive loop. Currently the only workaround is to ensure that such parent properties are excluded via the ignore list functionality of the compare function.

Comparing collections

If the direct compare check failed, but the property type supports IEnumerable, then some Linq is used to obtain the collection of items.

To save time, a count check is made and if the counts do not match (or one of the collections is null and the other is not), then an automatic mismatch is returned. If the counts do match, then all items are compared in the same manner as the parent objects.

IEnumerable&lt;object> collectionItems1;
IEnumerable&lt;object> collectionItems2;int collectionItemsCount1;int collectionItemsCount2;

collectionItems1 = ((IEnumerable)valueA).Cast&lt;object>();
collectionItems2 = ((IEnumerable)valueB).Cast&lt;object>();
collectionItemsCount1 = collectionItems1.Count();
collectionItemsCount2 = collectionItems2.Count();

I have tested this code on generic lists such as List<string>, and on strongly typed collections which inherit from Collection<TValue> with success.

The code

Below is the comparison code. Please note that it won't handle all situations - as mentioned indexed properties aren't supported. In addition, if you throw a complex object such as a DataReader I suspect it will throw a fit on that. I also haven't tested it on generic properties, it'll probably crash on those too. But it has worked nicely for the original purpose I wrote it for.

Also, as I was running this from a Console application, you may wish to replace the calls to Console.WriteLine with either Debug.WriteLine or even return them as an out parameter.

///&lt;summary>/// Compares the properties of two objects of the same type and returns if all properties are equal.///&lt;/summary>///&lt;param name="objectA">The first object to compare.&lt;/param>///&lt;param name="objectB">The second object to compre.&lt;/param>///&lt;param name="ignoreList">A list of property names to ignore from the comparison.&lt;/param>///&lt;returns>&lt;c>true&lt;/c> if all property values are equal, otherwise &lt;c>false&lt;/c>.&lt;/returns>publicstaticbool AreObjectsEqual(object objectA, object objectB, paramsstring[] ignoreList)
{bool result;if (objectA != null&& objectB != null)
  {
    Type objectType;

    objectType = objectA.GetType();

    result = true; // assume by default they are equalforeach (PropertyInfo propertyInfo in objectType.GetProperties(BindingFlags.Public | BindingFlags.Instance).Where(p => p.CanRead && !ignoreList.Contains(p.Name)))
    {object valueA;object valueB;

      valueA = propertyInfo.GetValue(objectA, null);
      valueB = propertyInfo.GetValue(objectB, null);// if it is a primative type, value type or implements IComparable, just directly try and compare the valueif (CanDirectlyCompare(propertyInfo.PropertyType))
      {if (!AreValuesEqual(valueA, valueB))
        {
          Console.WriteLine("Mismatch with property '{0}.{1}' found.", objectType.FullName, propertyInfo.Name);
          result = false;
        }
      }// if it implements IEnumerable, then scan any itemselseif (typeof(IEnumerable).IsAssignableFrom(propertyInfo.PropertyType))
      {
        IEnumerable&lt;object> collectionItems1;
        IEnumerable&lt;object> collectionItems2;int collectionItemsCount1;int collectionItemsCount2;// null checkif (valueA == null&& valueB != null || valueA != null&& valueB == null)
        {
          Console.WriteLine("Mismatch with property '{0}.{1}' found.", objectType.FullName, propertyInfo.Name);
          result = false;
        }elseif (valueA != null&& valueB != null)
        {
          collectionItems1 = ((IEnumerable)valueA).Cast&lt;object>();
          collectionItems2 = ((IEnumerable)valueB).Cast&lt;object>();
          collectionItemsCount1 = collectionItems1.Count();
          collectionItemsCount2 = collectionItems2.Count();// check the counts to ensure they matchif (collectionItemsCount1 != collectionItemsCount2)
          {
            Console.WriteLine("Collection counts for property '{0}.{1}' do not match.", objectType.FullName, propertyInfo.Name);
            result = false;
          }// and if they do, compare each item... this assumes both collections have the same orderelse
          {for (int i = 0; i &lt; collectionItemsCount1; i++)
            {object collectionItem1;object collectionItem2;
              Type collectionItemType;

              collectionItem1 = collectionItems1.ElementAt(i);
              collectionItem2 = collectionItems2.ElementAt(i);
              collectionItemType = collectionItem1.GetType();

              if (CanDirectlyCompare(collectionItemType))
              {if (!AreValuesEqual(collectionItem1, collectionItem2))
                {
                  Console.WriteLine("Item {0} in property collection '{1}.{2}' does not match.", i, objectType.FullName, propertyInfo.Name);
                  result = false;
                }
              }elseif (!AreObjectsEqual(collectionItem1, collectionItem2, ignoreList))
              {
                Console.WriteLine("Item {0} in property collection '{1}.{2}' does not match.", i, objectType.FullName, propertyInfo.Name);
                result = false;
              }
            }
          }
        }
      }elseif (propertyInfo.PropertyType.IsClass)
      {if (!AreObjectsEqual(propertyInfo.GetValue(objectA, null), propertyInfo.GetValue(objectB, null), ignoreList))
        {
          Console.WriteLine("Mismatch with property '{0}.{1}' found.", objectType.FullName, propertyInfo.Name);
          result = false;
        }
      }else
      {
        Console.WriteLine("Cannot compare property '{0}.{1}'.", objectType.FullName, propertyInfo.Name);
        result = false;
      }
    }
  }else
    result = object.Equals(objectA, objectB);return result;
}///&lt;summary>/// Determines whether value instances of the specified type can be directly compared.///&lt;/summary>///&lt;param name="type">The type.&lt;/param>///&lt;returns>///&lt;c>true&lt;/c> if this value instances of the specified type can be directly compared; otherwise, &lt;c>false&lt;/c>.///&lt;/returns>privatestaticbool CanDirectlyCompare(Type type)
{returntypeof(IComparable).IsAssignableFrom(type) || type.IsPrimitive || type.IsValueType;
}///&lt;summary>/// Compares two values and returns if they are the same.///&lt;/summary>///&lt;param name="valueA">The first value to compare.&lt;/param>///&lt;param name="valueB">The second value to compare.&lt;/param>///&lt;returns>&lt;c>true&lt;/c> if both values match, otherwise &lt;c>false&lt;/c>.&lt;/returns>privatestaticbool AreValuesEqual(object valueA, object valueB)
{bool result;
  IComparable selfValueComparer;

  selfValueComparer = valueA as IComparable;if (valueA == null&& valueB != null || valueA != null&& valueB == null)
    result = false; // one of the values is nullelseif (selfValueComparer != null&& selfValueComparer.CompareTo(valueB) != 0)
    result = false; // the comparison using IComparable failedelseif (!object.Equals(valueA, valueB))
    result = false; // the comparison using Equals failedelse
    result = true; // matchreturn result;
}

I hope you find these helper methods useful, this article will be updated if and when the methods are expanded with new functionality.

All content Copyright © by Cyotek Ltd or its respective writers. Permission to reproduce news and web log entries and other RSS feed content in unmodified form without notice is granted provided they are not used to endorse or promote any products or opinions (other than what was expressed by the author) and without taking them out of context. Written permission from the copyright owner must be obtained for everything else.
Original URL of this content is http://www.cyotek.com/blog/comparing-the-properties-of-two-objects-via-reflection?source=rss


Using the XmlReader class with C#

$
0
0

Some of the project files created by Cyotek Sitemap Creator and WebCopy are fairly large and the load performance of such files is poor. The files are saved using a XmlWriter class which is nice and fast. When reading the files back however, currently the whole file is loaded into a XmlDocument and then XPath expressions are used to pull out the values. This article describes our effort at converting the load code to use a XmlReader instead.

Sample XML

The following XML snippet can be used as a base for testing the code in this article, if required.

<?xmlversion="1.0"encoding="utf-8"standalone="yes"?><cyotek.webcopy.projectversion="1.0.0.0"generator="Cyotek WebCopy 1.0.0.2 (BETA))" edition=""><urilastCrawled="-8589156546443756722"includeSubDomains="false">http://saturn/cyotekdev/</uri><additionalUri><uri>first url</uri><uri>second url</uri></additionalUri><authenticationdoNotAskForPasswords="false"><credentialuri="/"userName="username"password="password"/></authentication><saveFolderpath="C:\Downloaded Web Sites"emptyBeforeCrawl="true"createFolderForDomain="true"flattenWebsiteDirectories="false"remapExtensions="true"/><crawlerremoveFragments="true"followRedirects="true"disableUriRemapping="false"slashedRootRemapMode="1"sort="false"acceptDeflate="true"acceptGZip="true"bufferSize="0"crawlAboveRoot="false"/><defaultDocuments/><linkInfosave="true"clearBeforeCrawl="true"/><stripQueryString>false</stripQueryString><useHeaderChecking>true</useHeaderChecking><userAgentuseDefault="true"></userAgent><rules><ruleoptions="1"enabled="true">trackback\?id=</rule><ruleoptions="1"enabled="false">/downloads/get</rule><ruleoptions="1"enabled="false">/article</rule><ruleoptions="1"enabled="false">/sitemap</rule><ruleoptions="1"enabled="false">image/get/</rule><ruleoptions="1"enabled="false">products</rule><ruleoptions="1"enabled="false">zipviewer</rule></rules><domainAliases><alias>(?:http(?:s?):\/\/)?saturn/cyotekdev/</alias></domainAliases><forms><pagename="" uri="login"enabled="true"method="POST"><parameters><parametername="rememberMe">true</parameter><parametername="username">username</parameter><parametername="password">password</parameter></parameters></page></forms><linkMap><linkid="b1b85626f9984279b5e033c30a0a3f65"uri="" source="1"contentType="text/html"httpStatus="200"lastDownloaded="-8589156550177150260"hash="0333961593BD555C49ABF2355140225A07DA9297"fileName="index.htm"><title>Cyotek</title><incomingLinks><linkid="b1b85626f9984279b5e033c30a0a3f65"/></incomingLinks><outgoingLinks><linkid="96a358d21135449eb6561f25399e24de"/></outgoingLinks><headers><headerkey="Content-Encoding"value="gzip"/><headerkey="Vary"value="Accept-Encoding"/><headerkey="X-AspNetMvc-Version"value="1.0"/><headerkey="Content-Length"value="3415"/><headerkey="Cache-Control"value="private"/><headerkey="Content-Type"value="text/html; charset=utf-8"/><headerkey="Date"value="Fri, 01 Oct 2010 16:51:07 GMT"/><headerkey="Expires"value="Fri, 01 Oct 2010 16:51:07 GMT"/><headerkey="ETag" value=""/><headerkey="Server"value="Microsoft-IIS/7.5"/><headerkey="X-Powered-By"value="UrlRewriter.NET 2.0.0"/></headers></link></linkMap></cyotek.webcopy.project>

Writing XML using a XmlWriter

Before I start discussing how to load the data, here is a quick overview of how it is originally saved. For clarity I'm only showing the bare bones of the method.

string workFile;

workFile = Path.GetTempFileName();

using (FileStream stream = File.Create(workFile))
{
  XmlWriterSettings settings;

  settings = new XmlWriterSettings { Indent = true, Encoding = Encoding.UTF8 };using (XmlWriter writer = XmlWriter.Create(stream, settings))
  {
    writer.WriteStartDocument(true);

      writer.WriteStartElement("uri");if (this.LastCrawled.HasValue)
        writer.WriteAttributeString("lastCrawled", this.LastCrawled.Value.ToBinary());
      writer.WriteAttributeString("includeSubDomains", _includeSubDomains);
      writer.WriteValue(this.Uri);
      writer.WriteEndElement();

    writer.WriteEndDocument();
  }
}

File.Copy(workFile, fileName, true);
File.Delete(workFile);

The above code creates a new temporary file and opens this into a FileSteam. A XmlSettings object is created to specify some options (by default it won't indent, making the output files difficult to read if you open then in a text editor), and then a XmlWriter is created from both the settings and stream.

Once you have a writer, you can quickly save data in compliant format, with the caveat that you must ensure that your WriteStarts have a corresponding WriteEnd, that you only have a single document element, and so on.

Assuming the writer gets to the end without any errors, the stream is closed, then temporary file is copied to the final destination before being deleted. (This is a good tip in its own right, as this means you won't destroy the user's existing if an error occurs, which you would if you directly wrote to the destination file.)

Reading XML using a XmlDocument

As discussed above, currently we use a XmlDocument to load data. The following snippet shows an example of this.

Note that the code below won't work "out of the box" as we use a number extension methods to handle data type conversion, which makes the code a lot more readable!

document = new XmlDocument();
document.Load(fileName);

_uri = documentElement.SelectSingleNode("uri").AsString();
_lastCrawled = documentElement.SelectSingleNode("uri/@lastCrawled").AsDate();
_includeSubDomains = documentElement.SelectSingleNode("uri/@includeSubDomains").AsBoolean(false);

So, as you can see we load a XmlDocument with the contents of our file. We then call SelectSingleNode several times with a different XPath expression.

And in the case of a crawler project, we do this a lot, as there is a large amount of information stored in the file.

I haven't tried to benchmark XPath, but I would assume that we could have optimized this by first getting the appropriate element (uri in this case) and then run additional XPath to read text/attributes. But this article would be rather pointless then as we want to discuss the XmlReader!

As an example, we have a 2MB project file which represents the development version of cyotek.com. Using System.Diagnostics.Stopwatch we timed how long it took to load this project 10 times, and it averaged 25seconds per load. Which is definitely unacceptable.

Reading using a XmlReader

Which brings us to the point of this article, doing the job using a XmlReader and hopefully improving the performance dramatically.

Before we continue though, a caveat:

This is the first time I've tried to use the XmlReader class, therefore it is possible this article doesn't take the best approach. I also wrote this article at the same time as getting the reader to work in my application so I've gone back and forth already correcting errors and misconceptions, which at times (and possible still) left the article a little disjointed. If you spot any errors in this article, please let us know.

The XmlReader seems to operate in the same principle as the XmlWriter, in that you need to read the data in more or less the same order as it was written. I suppose the most convenient analogy is a forward cursor in SQL Server, where you can only move forward through the records and not back.

Creating the reader

So, first things first - we need to create an object. But the XmlReader (like the XmlWriter) is abstract. Fortunately exactly like the writer, there is a static Create method we can use.

Continuing in the reader-is-just-like-writer vein, there is also a XmlReaderSettings class which you can use to fine tune certain aspects.

Lets get the document opened then. Unlike XmlDocument where you just provide a file name, XmlReader uses a stream.

using (FileStream fileSteam = File.OpenRead(fileName))
{
  XmlReaderSettings settings;

  settings = new XmlReaderSettings();
  settings.ConformanceLevel = ConformanceLevel.Document;using(XmlReader reader = XmlReader.Create(fileSteam, settings))
  {
  }
}

This sets us up nicely. Continuing my analogy from earlier, if you're familiar with record sets, there's usually a MoveNext or a Read method you call to read the next record in the set. The XmlReader doesn't seem to be different in this respect, as there's a dedicated Read method for iterating through all elements in the document. In addition, there's a number of other read methods for performing more specific actions.

There's also a NodeType property which lets you know what the current node type is, such as the start of an element, or the end of an element.

I'm going to use the IsStartElement method to work out if the current node is the start of an element, then perform processing based on the element name.

Enumerating elements, regardless of their position in the hierarchy

The following snippet will iterate all nodes and check to see if they are the start of an element. Note that this includes top level elements and child elements.

while (reader.Read())
{if (reader.IsStartElement())
  {
  }
}

The Name property will return the name of the active node. So I'm going to compare the name against the names written into the XML and do custom processing for each.

switch (reader.Name)
{case"uri":break;
}

Reading attributes on the active element

I mentioned above that there are a number of Read* methods. There are also several Move* methods. The one that caught my eye is MoveToNextAttribute, which I'm going to use for converting attributes to property values.

The Value property will return the value of the current node. If MoveToNextAttribute returns true, then I know I'm in a valid attribute and I can use the aforementioned Name property and the Value property to update property assignments.

The following snipped demonstrates the MoveToNextAttribute method and Value property:

while (reader.MoveToNextAttribute())
{switch (reader.Name)
  {case"lastCrawled":if (!string.IsNullOrEmpty(reader.Value))
        _lastCrawled = DateTime.FromBinary(Convert.ToInt64(reader.Value));break;case"includeSubDomains":if (!string.IsNullOrEmpty(reader.Value))
        _includeSubDomains = Convert.ToBoolean(reader.Value);break;
  }
}

This is actually quite a lot of work. Another alternative is to use the GetAttribute method - this reads an attribute value without moving the reader. I found this very handy when I was loading an object who's identifying property wasn't the first attribute in the XML block. It also takes up a lot less code

entry.Headers.Add(reader.GetAttribute("key"), reader.GetAttribute("value"));

Reading the content value of an element

I've now got two values out of hundreds in the file loaded and I'm finished with that element. Or am I? Actually I'm not - the original save code demonstrates that in addition to a pair of attributes, we're also saving data directly into to the element.

As we have been iterating attributes, the active node type is the last attribute, not the original element. Fortunately there's another method we can use - MoveToContent. This time though, we can't use the Value property. Instead, we'll call the ReadString method, giving us the following snippet:

if (reader.IsStartElement() || reader.MoveToContent() == XmlNodeType.Element)
  _uri = reader.ReadString();

I've included a call to IsStartElement in the above snippet as I found if I called MoveToContent when I was already on a content node (for example if no attributes were present), then it skipped the current node and moved to the next one.

If required, you can call ReadElementContentAsString instead of ReadString.

Some node values aren't strings though - in this case the XmlReader offers a number of strongly typed methods to return and convert the data for you, such as ReadElementContentAsBoolean, ReadElementContentAsDateTime, etc.

case"useHeaderChecking":
  _useHeaderChecking = reader.ReadElementContentAsBoolean();break;

Processing nodes where the same names are reused for different purposes

In the sample XML document at the start of this article, we have two different types of nodes named uri. The top level one has one purpose, and the children of additionalUri have another.

The problem we now face is as we have a single loop which processes all elements the case statement for uri will be triggered multiple times. We're going to need some way of determining which is which.

There are a few of ways we could do this, for example

  • Continue to use the main processing loop, just add a means of identifying which type of element is being processed
  • Adding another loop to process the children of the additionalUri element
  • Using the ReadSubtree method to create a brand new XmlReader containing the children and process that accordingly.

As we already have a loop which handles the elements we should probably reuse this - there'll be a lot of duplicate code if we suddenly start adding new loops.

Unfortunately there doesn't seem to an equivalent of the parent functionality of the XmlDocument class, the closest thing I could see was the Depth property. This returned 1 for the top level uri node, and 2 for the child versions. You need to be careful at what point you read this property, it also returned 2 when iterating the attributes of the top level uri node.

One workaround would be to use boolean flags to identify the type of node you are loading. This would also mean checking to see if the NodeType was XmlNodeType.EndElement, doing another name comparison, and resetting flags as appropriate. This might be more reliable (or understandable) than simply checking node depths, your mileage may vary.

Another alternative could be to combine depth and element start/end in order to push and pop a stack which would represent the current node hierarchy.

In order to get my converted code running, I've went with the boolean flag route. I suspect a future version of the crawler format is going to ensure the nodes have unique names so I don't have to do this hoop jumping again though!

Combined together, the load data code now looks like this:

while (reader.Read())
{if (reader.IsStartElement())
  {switch (reader.Name)
    {case"uri":if (!isLoadingAdditionalUris)
        {while (reader.MoveToNextAttribute())
          {switch (reader.Name)
            {case"lastCrawled":if (!string.IsNullOrEmpty(reader.Value))
                  _lastCrawled = DateTime.FromBinary(Convert.ToInt64(reader.Value));break;case"includeSubDomains":if (!string.IsNullOrEmpty(reader.Value))
                  _includeSubDomains = Convert.ToBoolean(reader.Value);break;
            }
          }if (reader.IsStartElement() || reader.MoveToContent() == XmlNodeType.Element)
            _uri = reader.ReadString();
        }elseif (reader.IsStartElement() || reader.MoveToContent() == XmlNodeType.EndElement)
          _additionalRootUris.Add(new Uri(UriHelpers.CombineUri(this.GetBaseUri(), reader.ReadString(), this.SlashedRootRemapMode)));break;case"additionalUri":
        isLoadingAdditionalUris = true;break;
    }
  }elseif (reader.NodeType == XmlNodeType.EndElement)
  {switch (reader.Name)
    {case"additionalUri":
        isLoadingAdditionalUris = false;break;
    }
  }
}

Which is significantly more code than the original version, and it's only handling a few values.

Using the ReadSubtree Method

The save functionality of crawler projects isn't centralized, child objects such as rules perform their own loading and saving via the following interface:

publicinterface IXmlPersistance
{void Write(string fileName, XmlWriter writer);void Read(string fileName, XmlNode reader);
}

And the current XmlDocument based code will call it like this:

_rules.Clear();foreach (XmlNode child in documentElement.SelectNodes("rules/rule"))
{
  Rule rule;
  rule = new Rule();
  ((IXmlPersistance)rule).Read(fileName, child);
  _rules.Add(rule);
}

None of this code will work now with the switch to use XmlReader so it all needs changing. For this, I'll create a new interface

publicinterface IXmlPersistance2
{void Write(string fileName, XmlWriter writer);void Read(string fileName, XmlReader reader);
}

The only difference is the Read method is now using a XmlReader rather than a XmlNode.

The next issue is that if I pass the original reader to this interface, the implementer will be able to read outside the boundaries of the element it is supposed to be reading, which could prevent the rest of the document from loading successfully.

We can resolve this particular issue by calling the ReadSubtree method which returns a brand new XmlReader object that only contains the active element and it's children. This means our other settings objects can happily (mis)use the passed reader without affecting the underlying load.

Note in the snippet below what we have wrapped the new reader in a using statement. The MSDN documentation states that the result of ReadSubtree should be closed before you continue reading from the original reader.

Rule rule;

rule = new Rule();using (XmlReader childReader = reader.ReadSubtree())
  ((IXmlPersistance2)rule).Read(fileName, childReader);
_rules.Add(rule);break;

Getting a XmlDocument from a XmlReader

One of the issues I did have was classes which extended the load behaviour of an existing class. For example, one abstract class has a number of base properties, which I easily converted to use XmlReader. However, this class is inherited by other classes and these load additional properties. Using the loop method outlined above it wasn't possible for these child classes to read their data as the reader had already been fully read. I didn't want to have these derived classes has to do the loading of base properties, and I didn't want to implement any half thought out idea. So, instead these classes continue to use the original loading of the XmlDocument. So, given a source of a XmlReader, how do you get an XmlDocument?

Turns out this is also very simple - the Load method of the XmlDocument can accept a reader. The only disadvantage is the constructor of the XmlDocument doesn't support this, which means you have to explicity declare a document, load it, then pass it on, demonstrated below.

void IXmlPersistance2.Read(string fileName, XmlReader reader)
{
  XmlDocument document;

  document = new XmlDocument();
  document.Load(reader);

  ((IXmlPersistance)this).Read(fileName, document.DocumentElement);
}

Fortunately these classes aren't used frequently and so they shouldn't adversely affect the performance tuning I'm trying to do.

I could have used the GetAttribute method I discussed earlier as this doesn't move the reader, but firstly I didn't discover that method until after I'd wrote this section of the article and I thought it had enough value to remain, and secondly I don't think there is an equivalent for elements.

The final verdict

Using the XmlReader is certainly long winded compared to the original code. The core of the original code is around 100 lines. The core of the new code is more than triple this. I'll probably replace all the "move to next attribute" loops with direct calls to GetAttribute which will cut down the amount of code a fair bit. I may also try to do a generic approach using reflection, although this will then have its own performance drawback.

However, the XML load performance increase was certainly worth the extra code - the average went from 25seconds down to 12seconds. This is still quite slow and I certainly want to improve it further, but at less than half the original load time I'm pleased with the result.

You also need to be careful when writing the document. In Cyotek crawler projects, as we are using XPath to query an entire document, we can load values no matter where they are located. When using a XmlReader, the values are read in the same order as they were written - so if you have saved a critical piece of information near the end of the document, but you require it when loading information at the start, you're going to run into problems.

All content Copyright © by Cyotek Ltd or its respective writers. Permission to reproduce news and web log entries and other RSS feed content in unmodified form without notice is granted provided they are not used to endorse or promote any products or opinions (other than what was expressed by the author) and without taking them out of context. Written permission from the copyright owner must be obtained for everything else.
Original URL of this content is http://www.cyotek.com/blog/using-the-xmlreader-class?source=rss

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.

All content Copyright © by Cyotek Ltd or its respective writers. Permission to reproduce news and web log entries and other RSS feed content in unmodified form without notice is granted provided they are not used to endorse or promote any products or opinions (other than what was expressed by the author) and without taking them out of context. Written permission from the copyright owner must be obtained for everything else.
Original URL of this content is http://www.cyotek.com/blog/mvc-actions-acceptverbs-head-requests-and-404-errors?source=rss

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)]publicnew DrawMode DrawMode
    {get { returnbase.DrawMode; }set { base.DrawMode = value; }
    }

    [Browsable(false), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden), EditorBrowsable(EditorBrowsableState.Never)]publicnewbool Sorted
    {get { returnbase.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.

protectedvirtual 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];
    }protectedvirtual 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.

publicevent EventHandler PreviewFontSizeChanged;

    [Category("Appearance"), DefaultValue(12)]publicint PreviewFontSize
    {get { return _previewFontSize; }set
      {
        _previewFontSize = value;this.OnPreviewFontSizeChanged(EventArgs.Empty);
      }
    }protectedvirtualvoid 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.

privatevoid 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.

publicvirtualvoid 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;
      }
    }

    protectedoverridevoid OnGotFocus(EventArgs e)
    {this.LoadFontFamilies();base.OnGotFocus(e);
    }protectedoverridevoid 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.

protectedoverridevoid OnMeasureItem(MeasureItemEventArgs e)
    {base.OnMeasureItem(e);if (e.Index > -1 && e.Index < this.Items.Count)
      {
        e.ItemHeight = _itemHeight;
      }
    }protectedoverridevoid 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.

protectedvirtualvoid 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;
    }privatebool IsUsingRTL(Control control)
    {bool result;if (control.RightToLeft == RightToLeft.Yes)
        result = true;elseif (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.

protectedoverridevoid Dispose(bool disposing)
    {this.ClearFontCache();if (_stringFormat != null)
        _stringFormat.Dispose();base.Dispose(disposing);
    }protectedvirtualvoid 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
{publicclass FontComboBox : ComboBox
  {#region  Private Member Declarations  private Dictionary&lt;string, Font> _fontCache;privateint _itemHeight;privateint _previewFontSize;private StringFormat _stringFormat;#endregion  Private Member Declarations  #region  Public Constructors  public FontComboBox()
    {
      _fontCache = new Dictionary&lt;string, Font>();this.DrawMode = DrawMode.OwnerDrawVariable;this.Sorted = true;this.PreviewFontSize = 12;this.CalculateLayout();this.CreateStringFormat();
    }#endregion  Public Constructors  #region  Events  publicevent EventHandler PreviewFontSizeChanged;#endregion  Events  #region  Protected Overridden Methods  protectedoverridevoid Dispose(bool disposing)
    {this.ClearFontCache();if (_stringFormat != null)
        _stringFormat.Dispose();base.Dispose(disposing);
    }protectedoverridevoid OnDrawItem(DrawItemEventArgs e)
    {base.OnDrawItem(e);if (e.Index > -1 && e.Index &lt; 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);
        }
      }
    }protectedoverridevoid OnFontChanged(EventArgs e)
    {base.OnFontChanged(e);this.CalculateLayout();
    }protectedoverridevoid OnGotFocus(EventArgs e)
    {this.LoadFontFamilies();base.OnGotFocus(e);
    }protectedoverridevoid OnMeasureItem(MeasureItemEventArgs e)
    {base.OnMeasureItem(e);if (e.Index > -1 && e.Index &lt; this.Items.Count)
      {
        e.ItemHeight = _itemHeight;
      }
    }protectedoverridevoid OnRightToLeftChanged(EventArgs e)
    {base.OnRightToLeftChanged(e);this.CreateStringFormat();
    }protectedoverridevoid 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  publicvirtualvoid 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)]publicnew DrawMode DrawMode
    {get { returnbase.DrawMode; }set { base.DrawMode = value; }
    }

    [Category("Appearance"), DefaultValue(12)]publicint PreviewFontSize
    {get { return _previewFontSize; }set
      {
        _previewFontSize = value;this.OnPreviewFontSizeChanged(EventArgs.Empty);
      }
    }

    [Browsable(false), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden), EditorBrowsable(EditorBrowsableState.Never)]publicnewbool Sorted
    {get { returnbase.Sorted; }set { base.Sorted = value; }
    }#endregion  Public Properties  #region  Private Methods  privatevoid 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;
      }
    }privatebool IsUsingRTL(Control control)
    {bool result;if (control.RightToLeft == RightToLeft.Yes)
        result = true;elseif (control.RightToLeft == RightToLeft.Inherit && control.Parent != null)
        result = IsUsingRTL(control.Parent);else
        result = false;return result;
    }#endregion  Private Methods  #region  Protected Methods  protectedvirtualvoid ClearFontCache()
    {if (_fontCache != null)
      {foreach (string key in _fontCache.Keys)
          _fontCache[key].Dispose();
        _fontCache.Clear();
      }
    }protectedvirtualvoid 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;
    }protectedvirtual 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];
    }protectedvirtual Font GetFont(string fontFamilyName, FontStyle fontStyle)
    {
      Font font;try
      {
        font = new Font(fontFamilyName, this.PreviewFontSize, fontStyle);
      }catch
      {
        font = null;
      }return font;
    }protectedvirtualvoid OnPreviewFontSizeChanged(EventArgs e)
    {if (PreviewFontSizeChanged != null)
        PreviewFontSizeChanged(this, e);this.CalculateLayout();
    }#endregion  Protected Methods  
  }
}

All content Copyright © by Cyotek Ltd or its respective writers. Permission to reproduce news and web log entries and other RSS feed content in unmodified form without notice is granted provided they are not used to endorse or promote any products or opinions (other than what was expressed by the author) and without taking them out of context. Written permission from the copyright owner must be obtained for everything else.
Original URL of this content is http://www.cyotek.com/blog/creating-a-wysiwyg-font-combobox-using-csharp?source=rss

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)]publicexternstaticint 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
  {protectedoverridevoid 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

All content Copyright © by Cyotek Ltd or its respective writers. Permission to reproduce news and web log entries and other RSS feed content in unmodified form without notice is granted provided they are not used to endorse or promote any products or opinions (other than what was expressed by the author) and without taking them out of context. Written permission from the copyright owner must be obtained for everything else.
Original URL of this content is http://www.cyotek.com/blog/enabling-shell-styles-for-the-listview-and-treeview-controls-in-csharp?source=rss

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.

All content Copyright © by Cyotek Ltd or its respective writers. Permission to reproduce news and web log entries and other RSS feed content in unmodified form without notice is granted provided they are not used to endorse or promote any products or opinions (other than what was expressed by the author) and without taking them out of context. Written permission from the copyright owner must be obtained for everything else.
Original URL of this content is http://www.cyotek.com/blog/migrating-from-visual-sourcesafe-to-subversion?source=rss

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

All content Copyright © by Cyotek Ltd or its respective writers. Permission to reproduce news and web log entries and other RSS feed content in unmodified form without notice is granted provided they are not used to endorse or promote any products or opinions (other than what was expressed by the author) and without taking them out of context. Written permission from the copyright owner must be obtained for everything else.
Original URL of this content is http://www.cyotek.com/blog/importing-a-sourcesafe-database-into-subversion?source=rss

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:

<?xmlversion="1.0"encoding="utf-8"?><SyntaxDefinitionname="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 /*:

<RuleSetignorecase="false"><Spanname="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 }.

<Spanname="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)

<RuleSetname="CssClass"ignorecase="true"><Spanname="Value"rule="ValueRules"bold="false"italic="false"color="Blue"stopateol="false"><Begincolor="Black">:</Begin><Endcolor="Black">;</End></Span><KeyWordsname="CSSLevel1PropertyNames"bold="false"italic="false"color="Red"><Keyword="background"/><Keyword="background-attachment"/>
        (snip)</KeyWords><KeyWordsname="CSSLevel2PropertyNames"bold="false"italic="false"color="Red"><Keyword="border-collapse"/><Keyword="border-spacing"/>
        (snip)</KeyWords><KeyWordsname="CSSLevel3PropertyNames"bold="false"italic="false"color="Red"><Keyword="@font-face"/><Keyword="@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:

<RuleSetname="ValueRules"ignorecase="false"><Spanname="Comment"bold="false"italic="false"color="Green"stopateol="false"><Begin>/*</Begin><End>*/</End></Span><<panname="String"bold="false"italic="false"color="BlueViolet"stopateol="true"><Begin>"</Begin><End>"</End></Span><Spanname="Char"bold="false"italic="false"color="BlueViolet"stopateol="true"><Begin>'</Begin><End>'</End></Span><KeyWordsname="Flags"bold="true"italic="false"color="BlueViolet"><Keyword="!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><Defaultcolor="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
    <Modefile="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.
    <?xmlversion="1.0"encoding="utf-8"?><SyntaxModesversion="1.0"><Modefile="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

All content Copyright © by Cyotek Ltd or its respective writers. Permission to reproduce news and web log entries and other RSS feed content in unmodified form without notice is granted provided they are not used to endorse or promote any products or opinions (other than what was expressed by the author) and without taking them out of context. Written permission from the copyright owner must be obtained for everything else.
Original URL of this content is http://www.cyotek.com/blog/css-syntax-highlighting-in-the-digitalrune-text-editor-control?source=rss


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)

protectedvirtual 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;
    }protectedvirtual 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:

protectedvirtual Font GetFont(string fontFamilyName)
    {
      Font font;using (FontFamily family = new FontFamily(fontFamilyName))
      {if (family.IsStyleAvailable(FontStyle.Regular))
          font = this.GetFont(fontFamilyName, FontStyle.Regular);elseif (family.IsStyleAvailable(FontStyle.Bold))
          font = this.GetFont(fontFamilyName, FontStyle.Bold);elseif (family.IsStyleAvailable(FontStyle.Italic))
          font = this.GetFont(fontFamilyName, FontStyle.Italic);elseif (family.IsStyleAvailable(FontStyle.Bold | FontStyle.Italic))
          font = this.GetFont(fontFamilyName, FontStyle.Bold | FontStyle.Italic);else
          font = null;
      }return font;
    }protectedvirtual Font GetFont(string fontFamilyName, FontStyle fontStyle)
    {returnnew 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.

publicbool DoesFontExist(string fontFamilyName, FontStyle fontStyle)
    {bool result;try
      {using (FontFamily family = new FontFamily(fontFamilyName))
          result = family.IsStyleAvailable(fontStyle);
      }catch (ArgumentException)
      {
        result = false;
      }return result;
    }

All content Copyright © by Cyotek Ltd or its respective writers. Permission to reproduce news and web log entries and other RSS feed content in unmodified form without notice is granted provided they are not used to endorse or promote any products or opinions (other than what was expressed by the author) and without taking them out of context. Written permission from the copyright owner must be obtained for everything else.
Original URL of this content is http://www.cyotek.com/blog/detecting-if-a-given-font-style-exists-in-csharp?source=rss

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:

protectedvirtual IDictionary&lt;GhostScriptCommand, object> GetConversionArguments(string pdfFileName, string outputImageFileName, int pageNumber, string password, Pdf2ImageSettings settings)
    {
      IDictionary&lt;GhostScriptCommand, object> arguments;

      arguments = new Dictionary&lt;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 sizeif (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 passwordif (!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:

publicvoid ConvertPdfPageToImage(string outputFileName, int pageNumber)
    {if (pageNumber < 1 || pageNumber > this.PageCount)thrownew 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)thrownew 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&lt;Bitmap> results;if (startPage < 1 || startPage > this.PageCount)thrownew ArgumentException("Start page number is out of bounds", "startPage");if (lastPage < 1 || lastPage > this.PageCount)thrownew ArgumentException("Last page number is out of bounds", "lastPage");elseif (lastPage < startPage)thrownew ArgumentException("Last page cannot be less than start page", "lastPage");

  results = new List&lt;Bitmap>();for (int i = startPage; i &lt;= 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

All content Copyright © by Cyotek Ltd or its respective writers. Permission to reproduce news and web log entries and other RSS feed content in unmodified form without notice is granted provided they are not used to endorse or promote any products or opinions (other than what was expressed by the author) and without taking them out of context. Written permission from the copyright owner must be obtained for everything else.
Original URL of this content is http://www.cyotek.com/blog/convert-a-pdf-into-a-series-of-images-using-csharp-and-ghostscript?source=rss

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 defaultsthis.GridDisplayMode = ImageBoxGridDisplayMode.None;this.BackColor = SystemColors.AppWorkspace;this.ImageBorderStyle = ImageBoxBorderStyle.FixedSingleDropShadow;// new pdf conversion settingsthis.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), "")]publicvirtual 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);
        }
      }
    }privatevoid SettingsPropertyChangedHandler(object sender, PropertyChangedEventArgs e)
    {this.OnSettingsChanged(e);
    }protectedvirtualvoid OnSettingsChanged(EventArgs e)
    {this.OpenPDF();if (this.SettingsChanged != null)this.SettingsChanged(this, e);
    }

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)]publicvirtualint PageCount
    { get { return _converter != null ? _converter.PageCount : 0; } }

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

          _currentPage = value;

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

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

    [Browsable(false)]publicbool CanMoveLast
    { get { returnthis.PageCount != 0 && this.CurrentPage != this.PageCount; } }

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

    [Browsable(false)]publicbool CanMovePrevious
    { get { returnthis.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.

publicvoid FirstPage()
    {this.CurrentPage = 1;
    }publicvoid LastPage()
    {this.CurrentPage = this.PageCount;
    }publicvoid NextPage()
    {this.CurrentPage++;
    }publicvoid 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.

publicevent EventHandler LoadingPage;publicevent 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.

publicvoid 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&lt;int, Bitmap>();
        _currentPage = 1;if (this.PageCount != 0)
        {
          _currentPage = 0;this.CurrentPage = 1;
        }
      }
    }privatevoid CleanUp()
    {// release  bitmapsif (this.PageCache != null)
      {foreach (KeyValuePair&lt;int, Bitmap> pair inthis.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.

protectedvirtualvoid 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

All content Copyright © by Cyotek Ltd or its respective writers. Permission to reproduce news and web log entries and other RSS feed content in unmodified form without notice is granted provided they are not used to endorse or promote any products or opinions (other than what was expressed by the author) and without taking them out of context. Written permission from the copyright owner must be obtained for everything else.
Original URL of this content is http://www.cyotek.com/blog/extending-the-imagebox-component-to-display-the-contents-of-a-pdf-file-using-csharp?source=rss

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;"publicbool IsElevated
{get
  {returnnew WindowsPrincipal(WindowsIdentity.GetCurrent()).IsInRole(WindowsBuiltInRole.Administrator);
  }
}publicvoid 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

All content Copyright © by Cyotek Ltd or its respective writers. Permission to reproduce news and web log entries and other RSS feed content in unmodified form without notice is granted provided they are not used to endorse or promote any products or opinions (other than what was expressed by the author) and without taking them out of context. Written permission from the copyright owner must be obtained for everything else.
Original URL of this content is http://www.cyotek.com/blog/detecting-if-an-application-is-running-as-an-elevated-process-and-spawning-a-new-process-using-elevated-permissions?source=rss

AngelCode bitmap font parsing using C#

$
0
0

Updated 10Jun2015: Code is now available on GitHub, any updates can be found there

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

privatevoid 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);
    }privatevoid 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

All content Copyright © by Cyotek Ltd or its respective writers. Permission to reproduce news and web log entries and other RSS feed content in unmodified form without notice is granted provided they are not used to endorse or promote any products or opinions (other than what was expressed by the author) and without taking them out of context. Written permission from the copyright owner must be obtained for everything else.
Original URL of this content is http://www.cyotek.com/blog/angelcode-bitmap-font-parsing-using-csharp?source=rss

Viewing all 559 articles
Browse latest View live