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

Announcing MantisSharp, a .NET client for using the MantisBT REST API

$
0
0

I've released a new open source project named MantisSharp, a simple .NET client for working with the recently introduced REST API for Mantis Bug Tracker.

The library is just getting started and is missing various functions (hello documentation!) but it seems to be usable - as well as the WinForms sample browser that I was using for development testing, I also tested it in an ASP.NET MVC application, both locally and then remotely using the development version of cyotek.com.

It's probably not ready for prime time, I need to add docs, samples and finally get serious about using await/async, plus get a .NET Standard build done. But I think it's getting off to a good start.

The GitHub repository can be found at https://github.com/cyotek/MantisSharp - the readme has lots of extra details so I'm not going to repeat it here.

Why create this library?

Originally I wanted to use the MantisBT REST API to automatically generate the product roadmaps on cyotek.com - currently these are manual, and looking at the last modification dates on the content entries shows the latest update was in 2015. Ouch. As I've been properly planning releases in our MantisBT instance, it made sense to use that data. However, I don't want to open access (anonymous or otherwise) to the MantisBT instance itself, hence deciding to use the new API they added recently.

I wasn't planning create a full blown library, I thought I'd just load the JSON into a dynamic and grab what I needed that way. But that untyped code offended me so much (and oddly enough there didn't seem to be another client out there from a very brief check of NuGet) that in the end it was inevitable.

Assuming more than just me uses this library I'd love to hear your feedback.

Getting Started

As well as the source, you can grab precompiled binaries via a NuGet package

Install-Package MantisSharp -Pre

The package includes builds for .NET 3.5, 4.0, 4.5 and 4.6. 4.7 will follow when I pave my machine and get the Creators Update, .NET Standard will follow as soon as I actually add it as a target and resolve any API issues.

Then just create an instance of the MantisClient, passing the base URI where your MantisBT installation is hosted, along with an API key. Also note that by default the REST API is disabled and needs to be explicitly switched on for external access. (There's a wiki page which tells you how).

MantisClient client = new MantisClient("YOUR_MANTIS_URI", "YOUR_API_KEY");

// list all projects
foreach (Project project in client.GetProjects())
{
  Console.WriteLine(project.Name);
}

// list all issues
foreach (Issue issue in client.GetIssues())
{
  Console.WriteLine(issue.Summary);
}

// list issues for a single project
var issues = client.GetIssues(4); // or pass in a Project reference

// get a single issue
Issue issue = client.GetIssue(52);

Known Issues

There's still outstanding work to do, some of which is detailed in the readme. I also haven't done much testing yet, and our MantisBT database is currently quite small, so I don't know how the library will perform under bigger databases.

Examples

An example of the WinForms demonstration application

An example of creating a roadmap type page using the REST API

All content Copyright (c) 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/announcing-mantissharp-a-net-client-for-using-the-mantisbt-rest-api?source=rss.


Writing custom Markdig extensions

$
0
0

Markdig, according to its description, "is a fast, powerful, CommonMark compliant, extensible Markdown processor for .NET". While most of our older projects use MarkdownDeep (including an increasingly creaky cyotek.com), current projects use Markdig and thus far it has proven to be an excellent library.

One of the many overly complicated aspects of cyotek.com is that in addition to the markdown processing, every single block of content is also ran through a byzantine number of regular expressions for custom transforms. When cyotek.com is updated to use Markdig, I definitely don't want these expressions to hang around. Enter, Markdig extensions.

Markdig extensions allow you extend Markdig to include additional transforms, things that might not conform to the CommonMark specification such as YAML blocks or pipe tables.

MarkdownPipeline pipline;
string html;
string markdown;

markdown = "# Header 1";

pipline = new MarkdownPipelineBuilder()
  .Build();

html = Markdown.ToHtml(markdown, pipline); // <h1>Header 1</h1>

pipline = new MarkdownPipelineBuilder()
  .UseAutoIdentifiers() // enable the Auto Identifiers extension
  .Build();

html = Markdown.ToHtml(markdown, pipline); // <h1 id="header-1">Header 1</h1>

Example of using an extension to automatically generate id attributes for heading elements.

I recently updated our internal crash aggregation system to be able to create MantisBT issues via our MantisSharp library. In these issues, stack traces include the line number or IL offset in the format #<number>. To my vague annoyance, Mantis Bug Tracker treats these as hyperlinks to other issues in the system in a similar fashion to how GitHub automatically links to issues or pull requires. It did however give me an idea to create a Markdig extension that performs the same functionality.

Deciding on the pattern

The first thing you need to do is decide the markdown pattern to trigger the extension. Our example is perhaps a bit too basic as it is a simple #<number>, whereas if you think of other issue systems such as JIRA, it would be <string>-<number>. As well as the "body" of the pattern you also need to consider the characters which surround it. For example, you might only allow white space, or perhaps brackets or braces - whenever I reference a JIRA issue I tend to surround them in square braces, e.g. [PRJ-1234].

The other thing to consider is the criteria of the core pattern. Using our example above, should we have a minimum number of digits before triggering, or a maximum? #999999999 is probably not a valid issue number!

Extension components

A Markdig extension is comprised of a few moving parts. Depending on how complicated your extension is, you may not need all parts, or could perhaps reuse existing parts.

  • The extension itself (always required)
  • A parser
  • A renderer
  • A object used to represent data in the abstract syntax tree (AST)
  • A object used to configure the extension functionality

In this plugin, I'll be demonstrating all of these parts.

Happily enough, there's actually already an extension built into Markdig for rendering JIRA links which was great as a getting started point, including the original MarkdigJiraLinker extension by Dave Clarke. As I mentioned at the start, Markdig has a lot of extensions, some simple, some complex - there's going to be a fair chunk of useful code in there to help you with your own.

Supporting classes

I'm actually going to create the components in a backwards order from the list above, as each step depends on the one before it, so it would make for awkward reading if I was referencing things that don't yet exist.

To get started with some actual code, I'm going to need a couple of supporting classes - an options object for configuring the extension (at the bare minimum we need to supply the base URI of a MantisBT installation), and also class to present a link in the AST.

First the options class. As well as that base URI, I'll also add an option to determine if the links generated by the application should open in a new window or not via the target attribute.

public class MantisLinkOptions
{
  public MantisLinkOptions()
  {
    this.OpenInNewWindow = true;
  }

  public MantisLinkOptions(string url)
    : this()
  {
    this.Url = url;
  }

  public MantisLinkOptions(Uri uri)
    : this()
  {
    this.Url = uri.OriginalString;
  }

  public bool OpenInNewWindow {get; set; }

  public string Url { get; set; }

Next up is the object which will present our link in the syntax tree. Markdig nodes are very similar to HTML, coming in two flavours - block and inline. In this article I'm only covering simple inline nodes.

I'm going to inherit from LeafInline and add a single property to hold the Mantis issue number.

There is actually a more specific LinkInline element which is probably a much better choice to use (as it also means you shouldn't need a custom renderer). However, I'm doing this example the "long way" so that when I move onto the more complex use cases I have for Markdig, I have a better understanding of the API.

[DebuggerDisplay("#{" + nameof(IssueNumber) + "}")]
public class MantisLink : LeafInline
{
  public StringSlice IssueNumber { get; set; }
}

String vs StringSlice

In the above class, I'm using the StringSlice struct offered by Markdig. You can use a normal string if you wish (or any other type for that matter), but StringSlice was specifically designed for Markdig to improve performance and reduce allocations. In fact, that's how I heard of Markdig to start with, when I read Alexandre's comprehensive blog post on the subject last year.

Creating the renderer

With the two supporting classes out the way, I can now create the rendering component. Markdig renderer's take an element from the AST and spit out some content. Easy enough - we create a class, inherit HtmlObjectRenderer<T> (where T is the name of your AST class, e.g. MantisLink) and override the Write method. If you are using a configuration class, then creating a constructor to assign that is also a good idea.

public class MantisLinkRenderer : HtmlObjectRenderer<MantisLink>
{
  private MantisLinkOptions _options;

  public MantisLinkRenderer(MantisLinkOptions options)
  {
    _options = options;
  }

  protected override void Write(HtmlRenderer renderer, MantisLink obj)
  {
    StringSlice issueNumber;

    issueNumber = obj.IssueNumber;

    if (renderer.EnableHtmlForInline)
    {
      renderer.Write("<a href=\"").Write(_options.Url).Write("view.php?id=").Write(issueNumber).Write('"');

      if (_options.OpenInNewWindow)
      {
        renderer.Write(" target=\"blank\" rel=\"noopener noreferrer\"");
      }

      renderer.Write('>').Write('#').Write(issueNumber).Write("</a>");
    }
    else
    {
      renderer.Write('#').Write(obj.IssueNumber);
    }
  }
}

So how does this work? The Write method we're overriding supplies the HtmlRenderer to write to, and the MantisLink object to render.

First we need to check if we should be rendering HTML by checking the EnableHtmlForInline property. If this is false, then we output the plain text, e.g. just the issue number and the # prefix.

If we are writing full HTML, then it's a matter of building a HTML a tag with the fully qualified URI generated from the base URI in the options object, and the AST node's issue number. We also add a target attribute if the options state that links should be in a new window. If we do add a target attribute I'm also adding a rel attribte as per MDN guidelines.

Notice how the HtmlRenderer objects Write method happily accepts string, char or StringSlice arguments, meaning we can mix and match to suit our purposes.

Creating the parser

With rendering out of the way, it's time for the most complex part of creating an extension - parsing it from a source document. For that, we need to inherit from InlineParser and overwrite the Match method, as well as setting up the characters that would trigger the parse routine - that single # character in our example.

public class MantisLinkInlineParser : InlineParser
{
  private static readonly char[] _openingCharacters =
  {
    '#'
  };

  public MantisLinkInlineParser()
  {
    this.OpeningCharacters = _openingCharacters;
  }

  public override bool Match(InlineProcessor processor, ref StringSlice slice)
  {
    bool matchFound;
    char previous;

    matchFound = false;

    previous = slice.PeekCharExtra(-1);

    if (previous.IsWhiteSpaceOrZero() || previous == '(' || previous == '[')
    {
      char current;
      int start;
      int end;

      slice.NextChar();

      current = slice.CurrentChar;
      start = slice.Start;
      end = start;

      while (current.IsDigit())
      {
        end = slice.Start;
        current = slice.NextChar();
      }

      if (current.IsWhiteSpaceOrZero() || current == ')' || current == ']')
      {
        int inlineStart;

        inlineStart = processor.GetSourcePosition(slice.Start, out int line, out int column);

        processor.Inline = new MantisLink
                            {
                              Span =
                              {
                                Start = inlineStart,
                                End = inlineStart + (end - start) + 1
                              },
                              Line = line,
                              Column = column,
                              IssueNumber = new StringSlice(slice.Text, start, end)
                            };

        matchFound = true;
      }
    }

    return matchFound;
  }
}

In the constructor, we set the OpeningCharacters property to a character array. When Markdig is parsing content, if it comes across any of the characters in this array it will automatically call your extension.

This neatly leads us onto the meat of this class - overriding the Match method. Here, we scan the source document and try to build up our node. If we're successful, we update the processor and let Markdig handle the rest.

We know the current character is going to be # as this is our only supported opener. However, we need to check the previous character to make sure that we try and process an distinct entity, and not a # character that happens to be in the middle of another string.

previous = slice.PeekCharExtra(-1);

if (previous.IsWhiteSpaceOrZero() || previous == '(' || previous == '[')

Here I use an extension method exposed by Markdig to check if the previous character was either whitespace, or nothing at all, i.e. the start of the document. I'm also checking for ( or [ characters in case the issue number has been wrapped in brackets or square braces.

If we pass this check, then it's time to parse the issue number. First we advance the character stream (to discard the # opener) and also initalize the values for creating a final StringSlice if we're successful.

slice.NextChar();

current = slice.CurrentChar;
start = slice.Start;
end = start;

As our GitHub/MantisBT issue numbers are just that, plain numbers, we simply keep advancing the stream until we run out of digits.

while (current.IsDigit())
{
  end = slice.Start;
  current = slice.NextChar();
}

As I'm going to work exclusively with the StringSlice struct, I'm only recording where the new slice will end. Even if you wanted to use a more traditional string, it probably makes sense to keep the above construct and then build your string at the end.

Once we've ran out of digits, we now essentially do a reverse of the check we made at the start - now we want to see if the next character is white space, the end of the stream, or a closing bracket/brace.

if (current.IsWhiteSpaceOrZero() || current == ')' || current == ']')

I didn't add a check for this, but potentially you should also look for matching pair - so if a bracket was used at the start, a closing bracket should therefore be present at the end.

Assuming this final check passes, that means we have a valid #<number> sequence, and so we create a new MantisLink object with the IssueNumber property populated with a brand new string slice. We then assign this new object to the Inline property of the processor.

inlineStart = processor.GetSourcePosition(slice.Start, out int line, out int column);

processor.Inline = new MantisLink
                    {
                      Span =
                      {
                        Start = inlineStart,
                        End = inlineStart + (end - start)
                      },
                      Line = line,
                      Column = column,
                      IssueNumber = new StringSlice(slice.Text, start, end)
                    };

I'm not sure if the Line and Column properties are used directly by Markdig, or if they are only for debugging or advanced AST scenarios. I'm also uncertain what the purpose of setting the Span property is - even though I based this code on the code from the Markdig repository, it doesn't seem to quite match up should I print out its contents. This leaves me wondering if I'm setting the wrong values. So far I haven't noticed any adverse effects though.

Creating the extension

The first thing to set up is the core extension. Markdig extensions implement the IMarkdownExtension interface. This simple interface exposes two overloads of a Setup method for configuring the parsing and rendering aspect of the extension.

One of these overloads is for customising the pipeline - we'll add our parser here. The second overload is for setting up the renderer. Depending on the nature of your extension you may only need one or the other.

As this class is responsible for creating any renders or parsers your extension needs, that also means it needs to have access to any required configuration classes to pass down.

public class MantisLinkerExtension : IMarkdownExtension
{
  private readonly MantisLinkOptions _options;

  public MantisLinkerExtension(MantisLinkOptions options)
  {
    _options = options;
  }

  public void Setup(MarkdownPipelineBuilder pipeline)
  {
    OrderedList<InlineParser> parsers;

    parsers = pipeline.InlineParsers;

    if (!parsers.Contains<MantisLinkInlineParser>())
    {
      parsers.Add(new MantisLinkInlineParser());
    }
  }

  public void Setup(MarkdownPipeline pipeline, IMarkdownRenderer renderer)
  {
    HtmlRenderer htmlRenderer;
    ObjectRendererCollection renderers;

    htmlRenderer = renderer as HtmlRenderer;
    renderers = htmlRenderer?.ObjectRenderers;

    if (renderers != null && !renderers.Contains<MantisLinkRenderer>())
    {
      renderers.Add(new MantisLinkRenderer(_options));
    }
  }
}

Firstly, I make sure the constructor accepts an argument of the MantisLinkOptions class to pass to the renderer.

In the Setup overload that configures the pipeline, I first check to make sure the MantisLinkInlineParser parser isn't already present; if not I add it.

In a very similar fashion, in the Setup overload that configures the renderer, I first check to see if a HtmlRenderer renderer was provided - after all, you could be using a custom renderer which wasn't HTML based. If I have got a HtmlRenderer renderer then I do a similar check to make sure a MantisLinkRenderer instance isn't present, and if not I create on using the provided options class and add it.

Adding an initialisation extension method

Although you could register extensions by directly manipulating the Extensions property of a MarkdownPipelineBuilder, generally Markdig extensions include an extension method which performs the boilerplate code of checking and adding the extension. The extension below checks to see if the MantisLinkerExtension has been registered with a given pipeline, and if not adds it with the specified options.

public static MarkdownPipelineBuilder UseMantisLinks(this MarkdownPipelineBuilder pipeline, MantisLinkOptions options)
{
  OrderedList<IMarkdownExtension> extensions;

  extensions = pipeline.Extensions;

  if (!extensions.Contains<MantisLinkerExtension>())
  {
    extensions.Add(new MantisLinkerExtension(options));
  }

  return pipeline;
}

Using the extension

MarkdownPipeline pipline;
string html;
string markdown;

markdown = "See issue #1";

pipline = new MarkdownPipelineBuilder()
  .Build();

html = Markdown.ToHtml(markdown, pipline); // <p>See issue #1</p>

pipline = new MarkdownPipelineBuilder()
  .UseMantisLinks(new MantisLinkOptions("https://issues.cyotek.com/"))
  .Build();

html = Markdown.ToHtml(markdown, pipline); // <p>See issue <a href="https://issues.cyotek.com/view.php?id=1" target="blank" rel="noopener noreferrer">#1</a></p>

Example of using an extension to automatically generate links for MantisBT issue numbers.

Wrapping up

In this article I showed how to introduce new inline elements parsed from markdown. This example at least was straightforward, however there is more that can be done. More advanced extensions such as pipeline tables have much more complex parsers that generate a complete AST of their own.

Markdig supports other ways to extend itself too. For example, the Auto Identifiers shown at the start of the article doesn't parse markdown but instead manipulates the AST even as it is being generated. The Emphasis Extra extension injects itself into another extension to add more functionality to that. There appears to be quite a few ways you can hook into the library in order to add your own custom functionality!

A complete sample project can be downloaded from the URL below or from the GitHub page for the project.

Although I wrote this example with Mantis Bug Tracker in mind, it wouldn't take very much effort at all to make it cover innumerable other websites.

Downloads

All content Copyright (c) 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/writing-custom-markdig-extensions?source=rss.

Capturing screenshots using C# and p/invoke

$
0
0

I was recently updating some documentation and wanted to programmatically capture some screenshots of the application in different states. This article describes how you can easily capture screenshots in your own applications.

Capturing a screenshot of the desktop

Using the Win32 API

This article makes use of a number of Win32 API methods. Although you may not have much call to use them directly in day to day .NET (not to mention Microsoft wanting everyone to use universal "apps" these days), they are still extraordinarily useful and powerful.

This article does assume you know the basics of platform invoke so I won't cover it here. In regards to the actual API's I'm using, you can find lots of information about them either on MSDN, or PInvoke.net.

A number of the API's used in this article are GDI calls. Generally, when you're using the Win32 GDI API, you need to do things in pairs. If something is created (pens, brushes, bitmaps, icons etc.), then it usually needs to be explicitly destroyed when finished with (there are some exceptions just to keep you on your toes). Although there haven't been GDI limits in Windows for some time now (as far as I know!), it's still good not to introduce memory leaks. In addition, device contexts always have a number of objects associated with them. If you assign a new object to a context, you must restore the original object when you're done. I'm a little rusty with this so hopefully I'm not missing anything out.

Setting up a device context for use with BitBlt

To capture a screenshot, I'm going to be using the BitBlt API. This copies information from one device context to another, meaning I'm going to need a source and destination context to process.

The source is going to be the desktop, so first I'll use the GetDesktopWindow and GetWindowDC calls to obtain this. As calling GetWindowDC essentially places a lock on it, I also need to release it when I'm finished with it.

IntPtr desktophWnd = GetDesktopWindow();
IntPtr desktopDc = GetWindowDC(desktophWnd);

// TODO

ReleaseDC(desktophWnd, desktopDc);

Now for the destination - for this, I'm going to create a memory context using CreateCompatibleDC. When you call this API, you pass in an existing DC and the new one will be created based on that.

IntPtr memoryDc = CreateCompatibleDC(desktopDc);

// TODO

DeleteDC(memoryDc);

There's still one last step to perform - by itself, that memory DC isn't hugely useful. We need to create and assign a GDI bitmap to it. To do this, first create a bitmap using CreateCompatibleBitmap and then attach it to the DC using SelectObject. SelectObject will also return the relevant old object which we need to restore (again using SelectObject) when we're done. We also use DeleteObject to clean up the bitmap.

IntPtr bitmap = CreateCompatibleBitmap(desktopDc, width, height);
IntPtr oldBitmap = SelectObject(memoryDc, bitmap);

// TODO

SelectObject(memoryDc, oldBitmap);
DeleteObject(bitmap);

Although this might seem like a lot of effort, it's not all that different from using objects implementing IDisposable in C#, just C# makes it a little easier with things like the using statement.

Calling BitBlt to capture a screenshot

With the above setup out the way, we have a device context which provides access to a bitmap of the desktop, and we have a new device context ready to transfer data to. All that's left to do is make the BitBlt call.

const int SRCCOPY = 0x00CC0020;
const int CAPTUREBLT = 0x40000000;

bool success = BitBlt(memoryDc, 0, 0, width, height, desktopDc, left, top, SRCCOPY | CAPTUREBLT);

if (!success)
{
  throw new Win32Exception();
}

If you've ever used the DrawImage method of a Graphics object before, this call should be fairly familiar - we pass in the DC to write too, along with the upper left corner where data will be copied (0, 0 in this example), followed by the width and height of the rectangle - this applies to both the source and destination. Finally, we pass in the source device context, and the upper left corner where data will be copied from, along with flags that detail how the data will be copied.

In my old VB6 days, I would just use SRCCOPY (direct copy), but in those days windows were simpler things. The CAPTUREBLT flag ensures the call works properly with layered windows.

If the call fails, I throw a new Win32Exception object without any parameters - this will take care of looking up the result code for the BitBlt failure and filling in an appropriate message.

Now that our destination bitmap has been happily "painted" with the specified region from the desktop we need to get it into .NET-land. We can do this via the FromHbitmap static method of the Image class - this method accepts a GDI bitmap handle and return a fully fledged .NET Bitmap object from it.

Bitmap result = Image.FromHbitmap(bitmap);

Putting it all together

As the above code is piecemeal, the following helper method will accept a Rectangle which describes which part of the desktop you want to capture and will then return a Bitmap object containing the captured information.

[DllImport("gdi32.dll")]
static extern bool BitBlt(IntPtr hdcDest, int nxDest, int nyDest, int nWidth, int nHeight, IntPtr hdcSrc, int nXSrc, int nYSrc, int dwRop);

[DllImport("gdi32.dll")]
static extern IntPtr CreateCompatibleBitmap(IntPtr hdc, int width, int nHeight);

[DllImport("gdi32.dll")]
static extern IntPtr CreateCompatibleDC(IntPtr hdc);

[DllImport("gdi32.dll")]
static extern IntPtr DeleteDC(IntPtr hdc);

[DllImport("gdi32.dll")]
static extern IntPtr DeleteObject(IntPtr hObject);

[DllImport("user32.dll")]
static extern IntPtr GetDesktopWindow();

[DllImport("user32.dll")]
static extern IntPtr GetWindowDC(IntPtr hWnd);

[DllImport("user32.dll")]
static extern bool ReleaseDC(IntPtr hWnd, IntPtr hDc);

[DllImport("gdi32.dll")]
static extern IntPtr SelectObject(IntPtr hdc, IntPtr hObject);

const int SRCCOPY = 0x00CC0020;

const int CAPTUREBLT = 0x40000000;

public Bitmap CaptureRegion(Rectangle region)
{
  IntPtr desktophWnd;
  IntPtr desktopDc;
  IntPtr memoryDc;
  IntPtr bitmap;
  IntPtr oldBitmap;
  bool success;
  Bitmap result;

  desktophWnd = GetDesktopWindow();
  desktopDc = GetWindowDC(desktophWnd);
  memoryDc = CreateCompatibleDC(desktopDc);
  bitmap = CreateCompatibleBitmap(desktopDc, region.Width, region.Height);
  oldBitmap = SelectObject(memoryDc, bitmap);

  success = BitBlt(memoryDc, 0, 0, region.Width, region.Height, desktopDc, region.Left, region.Top, SRCCOPY | CAPTUREBLT);
     
  try
  {
    if (!success)
    {
      throw new Win32Exception();
    }

    result = Image.FromHbitmap(bitmap);
  }
  finally
  {
    SelectObject(memoryDc, oldBitmap);
    DeleteObject(bitmap);
    DeleteDC(memoryDc);
    ReleaseDC(desktophWnd, desktopDc);
  }

  return result;
}

Note the try ... finally block used to try and free GDI resources if the BitBlt or FromHbitmap calls fail. Also note how the clean-up is the exact reverse of creation/selection.

Now that we have this method, we can use it in various ways as demonstrated below.

Capturing a single window

If you want to capture a window in your application, you could call Capture with the value of the Bounds property of your Form. But if you want to capture an external window then you're going to need to go back to the Win32 API. The GetWindowRect function will return any window's boundaries.

Win32 has its own version of .NET's Rectangle structure, named RECT. This differs slightly from the .NET version in that it has right and bottom properties, not width and height. The Rectangle class has a helper method, FromLTRB which constructs a Rectangle from left, top, right and bottom properties which means you don't need to perform the subtraction yourself.

Capturing a screenshot of a single window

[DllImport("user32.dll", SetLastError = true)]
public static extern bool GetWindowRect(IntPtr hwnd, out RECT lpRect);

[StructLayout(LayoutKind.Sequential)]
public struct RECT
{
  public int teft;
  public int top; 
  public int bight;
  public int bottom;
}

public Bitmap CaptureWindow(IntPtr hWnd)
{
  RECT region;

  GetWindowRect(hWnd, out region);

  return this.CaptureRegion(Rectangle.FromLTRB(region.Left, region.Top, region.Right, region.Bottom));
}

public Bitmap CaptureWindow(Form form)
{
  return this.CaptureWindow(form.Handle);
}

Depending on the version of Windows you're using, you may find that you get slightly unexpected results when calling Form.Bounds or GetWindowRect. As I don't want to digress to much, I'll follow up why and how to resolve in another post (the attached sample application includes the complete code for both articles).

Capturing the active window

As a slight variation on the previous section, you can use the GetForegroundWindow API call to get the handle of the active window.

[DllImport("user32.dll")]
static extern IntPtr GetForegroundWindow();

public Bitmap CaptureActiveWindow()
{
  return this.CaptureWindow(GetForegroundWindow());
}

Capturing a single monitor

.NET offers the Screen static class which provides access to all monitors on your system via the AllScreens property. You can use the FromControl method to find out which monitor a form is hosted on, and get the region that represents the monitor - with or without areas covered by the task bar and other app bars. This means it trivial to capture the contents of a given monitor.

Capturing a screenshot of a specific monitor

public Bitmap CaptureMonitor(Screen monitor)
{
  return this.CaptureMonitor(monitor, false);
}

public Bitmap CaptureMonitor(Screen monitor, bool workingAreaOnly)
{
  Rectangle region;

  region = workingAreaOnly ? monitor.WorkingArea : monitor.Bounds;

  return this.CaptureRegion(region);
}

public Bitmap CaptureMonitor(int index)
{
  return this.CaptureMonitor(index, false);
}

public Bitmap CaptureMonitor(int index, bool workingAreaOnly)
{
  return this.CaptureMonitor(Screen.AllScreens[index], workingAreaOnly);
}

Capturing the entire desktop

It is also quite simple to capture the entire desktop without having to know all the details of monitor arrangements. We just need to enumerate the available monitors and use Rectangle.Union to merge two rectangles together. When this is complete, you'll have one rectangle which describes all available monitors.

Capturing a screenshot of the entire desktop

public Bitmap CaptureDesktop()
{
  return this.CaptureDesktop(false);
}

public Bitmap CaptureDesktop(bool workingAreaOnly)
{
  Rectangle desktop;
  Screen[] screens;

  desktop = Rectangle.Empty;
  screens = Screen.AllScreens;

  for (int i = 0; i < screens.Length; i++)
  {
    Screen screen;

    screen = screens[i];

    desktop = Rectangle.Union(desktop, workingAreaOnly ? screen.WorkingArea : screen.Bounds);
  }

  return this.CaptureRegion(desktop);
}

There is one slight problem with this approach - if the resolutions of your monitors are different sizes, or are misaligned from each other, the gaps will be filled in solid black. It would be nicer to make these areas transparent, however at this point in time I don't need to capture the whole desktop so I'll leave this either as an exercise for the reader, or a subsequent update.

Capturing an arbitrary region

Of course, you could just call CaptureRegion with a custom rectangle to pick up some arbitrary part of the desktop. The above helpers are just that, helpers!

A note on display scaling and high DPI monitors

Although I don't have a high DPI monitor, I did temporarily scale the display to 125% to test that the correct regions were still captured. I tested with a manifest stating that the application supported high DPI and again without, in both cases the correct sized images were captured.

Capturing a scaled window that supports high DPI

Capturing a a scaled window that doesn't support high DPI

The demo program

A demonstration program for the techniques in this article is available from the links below. It's also available on GitHub.

Downloads

All content Copyright (c) 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/capturing-screenshots-using-csharp-and-p-invoke?source=rss.

Getting a window rectangle without the drop shadow

$
0
0

In my last article, I describe how to use the Win32 API to capture screenshots of the desktop. There was one frustrating problem with this however - when capturing an image based on the value of the Bounds property of a Form unexpected values were returned for the left position, width and height of the window, causing my screenshots to be too big.

An example of unexpected values when asking for window boundaries

I thought that was odd but as I wanted to be able to capture unmanaged windows in future then using Form.Bounds wasn't going to be possible anyway and I would have to use GetWindowRect. I'm sure that deep down in the Windows Forms code base it uses the same API so I was expecting to get the same "wrong" results, and I wasn't disappointed.

Although I'm calling these values "wrong", technically they are correct - here's another example this time using a plain white background.

Drop shadows appear around windows in Windows 10

As you can see, Windows 10 has a subtle drop shadow affect around three edges of a window, and it seems that is classed as being part of the window. This was surprising to me as I would assumed that it wouldn't be included being part of the OS theme rather than the developers deliberate choice.

Windows has the very handy hotkey Alt+Print Screen which will capture a screenshot of the active window and place it on the Clipboard. I've used this hotkey for untold years and it never includes a drop shadow, so clearly there's a way of excluding it. Some quick searching later reveals an answer - the DwmGetWindowAttribute function. This was introduced in Windows Vista and allows you to retrieve various extended aspects of a window, similar I think to GetWindowLong.

DWM stands for Desktop Window Manager and is the way that windows have been rendered since Vista, replacing the old GDI system.

There's a DWMWINDOWATTRIBUTE enumeration which lists the various supported attributes, but the one we need is DWMWA_EXTENDED_FRAME_BOUNDS. Using this attribute will return what I consider the window boundaries without the shadow.

const int DWMWA_EXTENDED_FRAME_BOUNDS = 9;

[DllImport("dwmapi.dll")]
static extern int DwmGetWindowAttribute(IntPtr hwnd, int dwAttribute, out RECT pvAttribute, int cbAttribute);

Calling it is a little bit more complicated that some other API's. The pvAttribute argument is a pointer to a value - and it can be of a number of different types. For this reason, the cbAttribute value must be filled in with the size of the value in bytes. This is a fairly common technique in Win32, although I'm more used to seeing cbSize as a member of a struct, not as a parameter on the call itself. Fortunately, we don't have to work this out manually as the Marshal class provides a SizeOf method we can use.

For sanities sake, I will also check the result code, and if it's not 0 (S_OK) then I'll fall back to GetWindowRect.

if (DwmGetWindowAttribute(hWnd, DWMWA_EXTENDED_FRAME_BOUNDS, out region, Marshal.SizeOf(typeof(RECT))) != 0)
{
  NativeMethods.GetWindowRect(hWnd, out region);
}

Now I have a RECT structure that describes what I consider to be the window boundaries.

A note on Windows versions

As the DwmGetWindowAttribute API was introduced in Windows Vista, if you want this code to work in Windows XP you'll need to check the current version of Windows. The easiest way is using Environment.OsVersion.

public Bitmap CaptureWindow(IntPtr hWnd)
{
  RECT region;

  if (Environment.OSVersion.Version.Major < 6)
  {
    GetWindowRect(hWnd, out region);
  }
  else
  {
    if (DwmGetWindowAttribute(hWnd, DWMWA_EXTENDED_FRAME_BOUNDS, out region, Marshal.SizeOf(typeof(RECT))) != 0)
    {
      GetWindowRect(hWnd, out region);
    }
  }

  return this.CaptureRegion(Rectangle.FromLTRB(region.teft, region.top, region.bight, region.bottom));
}

Although it should have no impact in this example, newer versions of Windows will lie to you about the version unless your application explicitly states that it is supported by the current Windows version, via an application manifest. This is another topic out of the scope of this particular article, but they are useful for a number of different cases.

Sample code

There's no explicit download to go with this article as it is all part of the Simple Screenshot Capture source code in the previous article.

All content Copyright (c) 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/getting-a-window-rectangle-without-the-drop-shadow?source=rss.

Book Review: The C# Helper Top 100

$
0
0

A not-great photo of some of my technical books

Almost 20 years ago after finishing a training scheme I was hired for my first permanent role. After getting paid for the first time I immediately rushed off to the bookshop to buy myself a programming book. The selection of books in my chosen language of the time (Visual Basic (not .NET!)) wasn't large, but in the end I bought Rod Stephens Custom Controls Library, which still graces my technical bookshelves even now. Since then I've bought many computing books, but I couldn't tell you the circumstances under which I bought most of them. This one however was the first and so I still remember it to this day - and this is also why if I'm asked for a technical book author Rod's name will be the first name that comes to mind. (For the curious, Dan Appleman would be the second, aside from the fact his Win32 book was awesome I'm pretty sure he takes the award for the longest titles in my collection).

Recently Rod announced he'd released a new self-published book, titled The C# Helper Top 100. Unsurprisingly given the title, this is a collection of the most popular posts on the C# Helper website. Although I've mostly given up on buying technical books that I barely read (plus I was fairly certain this book wasn't going to be my cup of tea), I decided I would buy it and review it as I have found some of Rod's posts to be helpful to me in the past. As I don't really enjoy reading these type of books electronically, I bought a paper copy from Amazon. (And also printed by Amazon according to the final page which I found interesting).

Full Disclosure. Unlike my earlier review of one of Rod's books I bought this out of my own pocket on a whim. Everything in this review are my own opinions and interpretations and my own words.

Trotting out old adages

The cover of The C# Helper Top 100

First impressions are generally important and I have to say I really dislike the cover - to me it feels very rough and ill fitting (and what's with all the different shades of green). But it should be all about the content, not the cover and I often have the same thoughts about a great many indie or self published fiction novels I buy and am (usually) nicely surprised by the stories within them.

Book layout and typesetting

Despite being a self published book, Rod has obviously taken some care to lay the book out properly and it compares well against other traditionally published books in my library. The only layout issues I spotted during my read were the odd jarring block of white space when a small paragraph is situated next to a tall image.

Who is the book aimed for

Rod is quite a prolific writer and he has penned many posts over the years, so I was curious as to what the selection of the top 100 would be. While they cover a surprising range of ground, I think most of them fall into the beginner to intermediate category - although there are some much more advanced examples dealing with mathematics and 3D rendering. On the whole though, I think this book would be more suited to beginner or junior developers. I found the book was approachable and easy to read.

Book Outline

The book is divided into a number of categories labelled as parts. Although the list might be daunting, each part is quite small - usually between 10 and 20 pages.

  • Serialization
  • Graphing
  • Text Processing
  • DataGridView
  • Microsoft Office Integration
  • WPF
  • Graphics
  • Image Processing
  • Cryptography
  • Dialogs
  • Internet
  • Miscellaneous Controls
  • Geometry
  • Algorithms
  • Three-Dimensional Programs
  • ListView and TreeView
  • System Interactions
  • Mathematics
  • Multimedia
  • Interoperability
  • Printing
  • Miscellany

Chapter Breakdown

This section is a bit long, so if you don't particularly care to read a description of all the contents, you can skip this section.

The first part, Serialization, is comprised of two chapters, one detailing a basic approach to loading data from CSV and the second converting objects to and from JSON using the DataContractJsonSerializer.

Next up is Graphing. This part includes several chapters on drawing line graphs in WPF, and drawing a histogram in Windows Forms.

Part 3 is Text Processing. The chapters in this section including transforms such as Pascal, Camel and Title case, along with a pair of useful chapters on converting file sizes into more friendly representations. I found some of the chapters in this part to be quite infuriating as they involve techniques such as the use of string concatenation in loops and regular expressions rather than something more efficient. However, eventually I conceded that over-complicating examples was a bad idea and so it was probably better to keep them simple. However, it's fair to say that most of the examples in this section ruffled my feathers.

Following is part 4, a very short part on the DataGridView control. Although the chapters in this section are quite brief, they do give a nice introduction to a control which can be difficult to use. I rarely use this control myself as generally I find it to be clunky.

Then we have part 5 dealing with Microsoft Office Integration. The chapters here describe reading and writing data from Excel workbooks, and writing data to Word documents. I read these chapters out of curiosity, but these days I like to avoid COM as much as possible and when I need to work with xlsx or docx files, I work with the file data directly and don't touch Office at all.

Part 6 concerns WPF. Given that many of the examples in the book deal with either WPF or Windows Forms I'm not too certain why this has it's own section but it's another short one. WPF is a technology I've never really used although I have seem some quite clean and nice looking applications written using WPF, as well as some dire ones. One of the chapters dealt with making a label blink and I'm astounded is a popular post - I seriously hope I don't come across an application where the developer is making labels blink.

Graphics is the subject matter for part 7. This part is comprised of multiple chapters for basic painting, print text (including aligned and justified text), including getting font metrics, but by using the Font object unlike my approach which used direct P/Invoke. It has another example on drawing rotated text which uses transforms on a Graphics object to do the work and seems an awful lot easier than the way I used to do it. Most of these examples using System.Drawing, but there is also a brief topic on custom rendering with WPF too.

The next part, Image Processing, is another reasonably chunky chapter. Part 8 starts off by describing how to save JPG files with custom compression values to compare the output. This example also details a method of cloning an image without keeping the source stream around, something that bit me quite a few years ago and is possibly worth an article of its own. This is followed by an example showing one way of making an image grey scale by manipulating bitmap bits directly, which is much better than using Bitmap.GetPixel and Bitmap.SetPixel. Other examples include cropping images, scaling images, selecting parts of images, comparing images and generating a Mandelbrot set. It then wraps up with a couple of examples on working with bitmaps in WPF. I was very surprised when I read these examples as it mentioned a Microsoft Windows Media Photo file format, a format I hadn't heard of before.

Part 9 is Cryptography. This part is quite small with only a pair of chapters. The first of these details how to encrypt or decrypt a file using AES encryption. The second involves generating cryptographically random numbers. After many of the other more basic examples so far, I was quite pleased to see something like this being highly ranked posts as these are quite advanced topics and rolling your own encryption is generally not a good idea.

Dialogs are the subject matter of part 10. There are two chapters to this part, the first dealing with various ways of allowing a user to select a folder, and the second for building a password dialog. The folder selection chapter starts reasonably enough, describing how .NET only provides access to the old browse for folder dialog instead of the newer one that was added to later versions of Windows. However, its recommendation to eschew using the WindowsAPICodePack NuGet package and instead use Excel via COM interop is a really curious recommendation and not one I agree with in the slightest. I don't see why you'd want to avoid using a third party package which you can distribute with your application and instead rely on a huge, bloated, and slow application which you definitely cannot distribute. This seems to be another quite popular post but I would hope people aren't writing applications that go an ask Excel to display a folder selection dialog. The second chapter is for building a password dialog and is an extremely basic example.

Part 11 is titled Internet. This part includes chapters on getting stock quotes, weather forecasts, and getting file attributes from an FTP server. There's also an example on using the WebBrowser control with custom HTML.

For some reason there isn't a part 12

Part 13 is named Miscellaneous Controls. This section includes a few short examples on working with Windows Forms controls, such as making a checkable group box (in a slightly odd fashion), finding controls by name, finding ListBox items using partial matching and wrapping up with making a RichTextBox control fit its contents.

Moving on we have part 14, Geometry. From a personal standpoint I found this to be one of the most useful part of the book as it dealt with topics I'm not hugely competent with and could be useful in some future projects of mine. The chapters include detecting where if two lines are touching, or where they would eventually intersect, finding the distance between lines and points, circle intersection, and a number of chapters on polygons, such as calculating areas, detecting if points are within a polygon and a few more.

As to be expected from Rod, we have part 15 on Algorithms although it is another quite short section. First up is generating all permutations of a set of values, a round robin tournament generator, and finally drawing a family tree. This last example could be reused for any hierarchy diagram really and could be a useful starting point for someone. In fact, while I didn't use this specific example, Rod has a couple of other examples for calculating tree layouts on C# Helper and I did use one of those a few years back as the basis for the sitemap diagrams in WebCopy.

Part 16 deals with Three-Dimensional Programs. This is a reasonably sized part that has a number of chapters dealing with rotation, applying textures, drawing smooth surfaces and drawing wireframes. All of these examples use WPF. While I scanned through the examples, it wasn't a thorough reading - I'm not really interested in trying to write a 3D game or have need for 3D in business applications.

After the trickiness of 3D, we go back to something simpler with part 17. The ListView and TreeView part has a number of introductory topics for the ListView control, detailing how to use icons, groups and perform custom sorting. This is followed by slightly more complicated article on printing a ListView which again I feel could be a very nice starting point for a reusable component for someones code base. The final chapter describes how to populate a TreeView with the contents of an XML document.

For part 18 Rod shares a number of chapters regarding System Interactions. Firstly it starts of by describing how to get detailed printer information using Windows Management Instrumentation (WMI). I have briefly encountered this in the past but never really used it so it was an interesting read, even if I do tend to do that sort of thing the "long way" with P/Invoke. Next it describes how to combine or resolve relative paths by using Path.GetFullPath, followed by an example on playing system sounds. It wraps up an interesting mix of chapters by describing how to get the serial number of a hard drive.

Part 19 is about Mathematics. I only scanned through this section for the usual reason. First up is a complicated looking example on solving a system of equations. This is followed by a pair of chapters on linear/polynomial least squares fit. Next is the Sieve of Eratosthenes and numbering factoring. It finishes of with a nice simple example of converting numbers between bases.

Part 20 deals with Multimedia. Briefly anyway, it's a very short part. The first example shows how to use animated GIFs by loading them into other controls like any other image. This example might have been more interesting if it used the ImageAnimator class instead to show how to manually animate an image (for example if you're doing custom painting in a ownerdraw control). It is then followed by an interesting article showing how to play videos using WPF.

Almost at the end is part 21 Interoperability. This is another interesting part which deals with working with other applications. It starts with a chapter on creating a COM DLL for use with Excel, then details how to use drag and drop. The part concludes with a pair of chapters dealing with the Clipboard. Aside from the COM DLL, the knowledge in these chapters should be part of every programmers toolkit as they are very useful techniques to add to applications and it is encouraging to see they are popular.

The penultimate part is 22, Printing. The first chapter details how to create a tabular report, in a very similar manner to the ListView printing example featured earlier in the book. This is then followed by a chapter demonstrating how to print multiple pages. Although short, printing support is another feature which I think can be quite important to have and this too is good to see as a popular topic.

And finally we have part 23, Miscellany. This starts by describing how to create a TypeConverter for use with a PropertyGrid control. Although these can be useful things to create (and I have wrote about them in the past), I am surprised they are that popular. Next up is a description of using Unicode symbols in your application (which sadly also applies to emoji). There is a useful chapter on using the BackgroundWorker component, which can be an excellent way of making your application do background tasks without getting into the complexity of threads or the TPL. The final chapter of the book is an article on using the Stopwatch class.

As you'd often expect with a technical book, the final section is an index, nothing much to add to that!

Getting the source code

The book doesn't come with a CD, but you can download the source directly from the book's web page (click Source Code in the page footer, this is a direct download).

I think it's a shame that there isn't a shortcut to the online versions of the articles, as this would make downloading individual samples easier, as well as being able to view comments or participate in them - sometimes I have found more value in the comments for an article than in the article itself (this is a general comment, not specific to the C# Helper blog), and quite often users post questions to Rod and he always seems to take the time to answer them no matter how inane they are.

Errata

Aside from the missing part 12, I didn't notice any other obvious errors when reading the book until I got to the end. I suspect the final section "Aferward" was probably supposed to be "Afterword", and for some reason all the Index pages are also titled as Afterward.

Closing Words

Another not-great photo of some more of my technical books

I found it quite a struggle to see what class of developer I would recommend read a book like this given the diverse subject matter. The random selection of articles within some sections mean I think it's very hit and miss on what people might be interested in. On the other hand, it's not a book focused on any one technology so this is perhaps to be expected.

Any intermediate or above developer is probably going to know everything that is relevant to their field without needing the book, and wouldn't be concerned with the chapters that weren't relevant.

If these really are the top 100 posts then there's some really odd examples. However, the C# Helper Blog has hundreds more posts, and is well worth a look if you're a budding C# developer. In fact, it's worth subscribing to the RSS feed anyway as while I'm not personally interested in many of the articles, something does occasionally pop up now and then to catch my eye. There's very few websites that I've been following for years; many of them disappeared long ago - this is one that has stayed the course.

This is only the second technical book review that I've written and I'm still trying to find my "voice" with them. Comments, feedback and constructive criticism welcome!

All content Copyright (c) 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/book-review-the-csharp-helper-top-100?source=rss.

Creating a GroupBox containing an image and a custom display rectangle

$
0
0

One of our applications required a GroupBox which was more like the one featured in the Options dialog of Microsoft Outlook 2003. This article describes how to create a custom GroupBox component which allows this type of user interface, and also a neat trick on adjusting the client area so that when you drag controls inside the GroupBox, the handy little margin guides allow you to position without overlapping the icon.

Add a new Component class to your project, and inherit this from the standard GroupBox.

    [ToolboxItem(true)]
    [DefaultEvent("Click"), DefaultProperty("Text")]
    public partial class GroupBox : System.Windows.Forms.GroupBox

I personally don't like assigning variables at the same time as defining them, so I've added a default constructor to assign the defaults and also to set-up the component as we need to set a few ControlStyles.

    public GroupBox()
    {
      _iconMargin = new Size(0, 6);
      _lineColorBottom = SystemColors.ButtonHighlight;
      _lineColorTop = SystemColors.ButtonShadow;

      this.SetStyle(ControlStyles.DoubleBuffer | ControlStyles.AllPaintingInWmPaint | ControlStyles.ResizeRedraw |
                    ControlStyles.UserPaint | ControlStyles.SupportsTransparentBackColor, true);

      this.CreateResources();
    }

Although this is a simple component, we need at the minimum an Image property to specify the image. We're also adding color properties in case we decide to use the component in a non-standard interface later on.

    private Size _iconMargin;
    private Image _image;
    private Color _lineColorBottom;
    private Color _lineColorTop;

    [Category("Appearance"), DefaultValue(typeof(Size), "0, 6")]
    public Size IconMargin
    {
      get { return _iconMargin; }
      set
      {
        _iconMargin = value;
        this.Invalidate();
      }
    }

    [Category("Appearance"), DefaultValue(typeof(Image), "")]
    public Image Image
    {
      get { return _image; }
      set
      {
        _image = value;
        this.Invalidate();
      }
    }

    [Category("Appearance"), DefaultValue(typeof(Color), "ButtonHighlight")]
    public Color LineColorBottom
    {
      get { return _lineColorBottom; }
      set
      {
        _lineColorBottom = value;
        this.CreateResources();
        this.Invalidate();
      }
    }

    [Category("Appearance"), DefaultValue(typeof(Color), "ButtonShadow")]
    public Color LineColorTop
    {
      get { return _lineColorTop; }
      set
      {
        _lineColorTop = value;
        this.CreateResources();
        this.Invalidate();
      }
    }

    [DefaultValue("")]
    public override string Text
    {
      get { return base.Text; }
      set
      {
        base.Text = value;
        this.Invalidate();
      }
    }

If you wanted you could create and destroy required GDI objects every time the control is painted, but in this example I've opted to create them once for the lifetime of the control. Therefore I've added CreateResources and CleanUpResources to create and destroy these. Although not demonstrated in this in-line listing, CleanUpResources is also called from the components Dispose method. You'll also notice CreateResources is called whenever a property value changes, and that it first releases resources in use.

    private void CleanUpResources()
    {
      if (_topPen != null)
        _topPen.Dispose();

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

      if (_textBrush != null)
        _textBrush.Dispose();
    }

    private void CreateResources()
    {
      this.CleanUpResources();

      _topPen = new Pen(_lineColorTop);
      _bottomPen = new Pen(_lineColorBottom);
      _textBrush = new SolidBrush(this.ForeColor);
    }

Now that all the initialization is performed, we're going to add our drawing routine which is to simply override the OnPaint method.

Remember that as we are overriding an existing component, we should override the base components methods whenever possible - this means overriding OnPaint and not hooking into the Paint event.

    protected override void OnPaint(PaintEventArgs e)
    {
      SizeF size;
      int y;

      size = e.Graphics.MeasureString(this.Text, this.Font);
      y = (int)(size.Height + 3) / 2;

      // draw the header text and line
      e.Graphics.DrawString(this.Text, this.Font, _textBrush, 1, 1);
      e.Graphics.DrawLine(_topPen, size.Width + 3, y, this.Width - 5, y);
      e.Graphics.DrawLine(_bottomPen, size.Width + 3, y + 1, this.Width - 5, y + 1);

      // draw the image
      if ((_image != null))
        e.Graphics.DrawImage(_image, this.Padding.Left + _iconMargin.Width, this.Padding.Top + (int)size.Height + _iconMargin.Height, _image.Width, _image.Height);

      //draw a designtime outline
      if (this.DesignMode)
      {
        Pen pen;
        pen = new Pen(SystemColors.ButtonShadow);
        pen.DashStyle = DashStyle.Dot;
        e.Graphics.DrawRectangle(pen, 0, 0, Width - 1, Height - 1);
        pen.Dispose();
      }
    }

In the code above you'll also notice a block specifically for design time. As this control only has borders at the top of the control, at design time it may not be obvious where the boundaries of the control are when laying out your interface. This code adds a dotted outline to the control at design time, and is ignored at runtime.

Another method we are overriding is OnSystemColorsChanged. As our default colors are based on system colors, should these change we need to recreate our objects and repaint the control.

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

      this.CreateResources();
      this.Invalidate();
    }

The client area of a standard group box accounts for the text header and the borders. Our component however, needs an additional offset on the left to account for the icon. If you try and place controls into the group box, you will see the snapping guides appear in the "wrong" place.

Fortunately however, it is very easy for us to suggest our own client area via the DisplayRectangle property. We just override this and provide a new rectangle which includes provisions for the width of the image.

    public override Rectangle DisplayRectangle
    {
      get
      {
        Size clientSize;
        int fontHeight;
        int imageSize;

        clientSize = base.ClientSize;
        fontHeight = this.Font.Height;

        if (_image != null)
          imageSize = _iconMargin.Width + _image.Width + 3;
        else
          imageSize = 0;

        return new Rectangle(3 + imageSize, fontHeight + 3, Math.Max(clientSize.Width - (imageSize + 6), 0), Math.Max((clientSize.Height - fontHeight) - 6, 0));
      }
    }

Now as you can see the snapping guides suggest a suitable left margin based on the current image width.

You can download the complete source for the GroupBox component below.

Downloads

All content Copyright (c) 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 https://www.cyotek.com/blog/creating-a-groupbox-containing-an-image-and-a-custom-display-rectangle?source=rss.

Error 80040154 when trying to use SourceSafe via interop on 64bit Windows

$
0
0

We recently moved to Windows 7, and I decided to go with the 64bit version for my machine. One of the utilities we use is a small tool for adding folders to Visual SourceSafe (why we haven't moved to another SCC provider yet is another question!) via the SourceSafeTypeLib interop dll. However, I was most annoyed when it wouldn't work on my machine, the following exception message would be displayed:

Retrieving the COM class factory for component with CLSID {783CD4E4-9D54-11CF-B8EE-00608CC9A71F} failed due to the following error: 80040154.

By default, .NET applications run using the CLR that matches your operating system, ie x64 on Windows 64bit, and x86 on Windows 32bit. I found that if I change the platform target from Any CPU to x86 (you can find this on the Build tab of your project's properties) to force it to use the 32bit CLR, then the interop would succeed and the utility would work again.

Hopefully this will be of use for the next person with this problem. Meanwhile I'm still thinking about a new SCC provider :)

All content Copyright (c) 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 https://www.cyotek.com/blog/error-80040154-when-trying-to-use-sourcesafe-via-interop-on-64bit-windows?source=rss.

Using XSLT to display an ASP.net sitemap without using tables

$
0
0

The quick and easy way of displaying an ASP.net site map (web.sitemap) in an ASP.net page is to use a TreeView control bound to a SiteMapDataSource component as shown in the following example:

<asp:SiteMapDataSource runat="server" ID="siteMapDataSource" EnableViewState="False"   ShowStartingNode="False" /><asp:TreeView runat="server" ID="siteMapTreeView" DataSourceID="siteMapDataSource"  EnableClientScript="False" EnableViewState="False" ShowExpandCollapse="False"></asp:TreeView>

Which results in a mass of nested tables, in-line styles, and generally messy mark-up.

With just a little more effort however, you can display the sitemap using a XSLT transform, resulting in slim, clean and configurable mark-up - and not a table to be seen.

This approach can be used with both Web Forms and MVC.

This article assumes you already have a pre-made ASP.net sitemap file.

Defining the XSLT

Add a new XSLT File to your project. In this case, it's named sitemap.xslt.

Next, paste in the mark-up below.

<?xml version="1.0" encoding="utf-8"?><xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:map="http://schemas.microsoft.com/AspNet/SiteMap-File-1.0" exclude-result-prefixes="map"><xsl:output method="xml" encoding="utf-8" indent="yes"/><xsl:template name="mapNode" match="map:siteMap"><ul><xsl:apply-templates/></ul></xsl:template><xsl:template match="map:siteMapNode"><li><a href="http://cyotek.com{substring(@url, 2)}" title="{@description}"><xsl:value-of select="@title"/></a><xsl:if test="map:siteMapNode"><xsl:call-template name="mapNode"/></xsl:if></li></xsl:template></xsl:stylesheet>

Note: As generally all URL's in ASP.net site maps start with ~/, the href tag in the above example has been customized to include the domain http://cyotek.com at the start, then use the XSLT substring function to strip the ~/ from the start of the URL. Don't forget to modify the URL to point to your own domain!

Declaratively transforming the document

If you are using Web forms controls, then this may be the more convenient approach for you.

Just add the XML component to your page, and set the DocumentSource property to the name of the sitemap, and the TransformSource property to the name of your XSLT file.

<asp:Xml runat="server" ID="xmlSiteMapViewer" DocumentSource="~/web.sitemap" TransformSource="~/sitemap.xslt" />

Programmatically transforming the document

The ASP.net XML control doesn't need to be inside a server side form tag, so you can use the exact same code above in your MVC views.

However, if you want to do this programmatically, the following code works too.

  var xmlFileName = Server.MapPath("~/web.sitemap");
  var xslFileName=Server.MapPath("~/sitemap.xslt");
  var result =new System.IO.StringWriter();
  var transform = new System.Xml.Xsl.XslCompiledTransform();

  transform.Load(xslFileName);
  transform.Transform(xmlFileName, null, result);

  Response.Write(result.ToString());

The result

The output of the transform will be simple series of nested unordered lists, clean and ready to be styled with CSS. And for little more effort than it took to do the original tree view solution.

With a bit more tweaking you can probably expand this to show only a single branch, useful for navigation within a section of a website, or creating breadcrumb trails.

All content Copyright (c) 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 https://www.cyotek.com/blog/using-xslt-to-display-an-asp-net-sitemap-without-using-tables?source=rss.


Converting BBCode into HTML using C#

$
0
0

Although the dynamic content in the Cyotek website is written using Markdown syntax using the MarkdownSharp library, we decided to use the more commonly used BBCode tags for the forums.

Some of the source code on this site is also preformatted using the CSharpFormat library, and we wanted to provide access to this via forum tags too.

A quick Google search brought up a post by Mike343 which had a BBCode parser that more or less worked, but didn't cover everything we wanted.

You can download below an updated version of this parser which has been modified to correct some problems with the original implementation and add some missing BBCode tags, including a set of custom tags for providing the syntax highlighting offered by CSharpFormat. Using the provided formatter classes you can easily create additional tags to suit the needs of your application.

To transform a block of BBCode into HTML, call the static Format method of the BbCodeProcessor class, for example:

string exampleBbcCode = "[b]this text is bold[/b]\n[i]this text is italic[/i]\n[u]this text is underlined[/u]";
string html = BbCodeProcessor.Format(exampleBbcCode);

is transformed into

<p><strong>this text is bold</strong><br><em>this text is italic</em><br><u>this text is underlined</u></p>

Much of the formatting is also customisable via CSS - several of the BBCode tags such as [code], [quote], [list] etc are assigned a class which you can configure in your style sheets. Listed below are the default rules used by the Cyotek site as a starting point for your own:

.bbc-codetitle, .bbc-quotetitle { margin: 1em 1.5em 0; padding: 2px 4px; background-color: #A0B3CA; font-weight: bold; }
.bbc-codecontent, .bbc-quotecontent { margin: 0 1.5em 1em; padding: 5px; border: solid 1px #A0B3CA; background-color: #fff; }
.bbc-codecontent pre { margin: 0; padding: 0; }
.bbc-highlight { background-color: #FFFF00; color: #333399; }
.bbc-spoiler { color: #C0C0C0; background-color: #C0C0C0; }
.bbc-indent { padding: 0 1em; }
.bbc-list { margin: 1em; }

Finally, if you are using MVC, you may find the following HTML Helper useful for transforming code from within your views.

public static string FormatBbCode(this HtmlHelper helper, string text)
{
   return BbCodeProcessor.Format(helper.Encode(text));
}

If you create any additional formatting codes for use with this library, please let us know via either comments or the Contact Us link, and we'll integrate them into the library for others to use.

Downloads

All content Copyright (c) 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 https://www.cyotek.com/blog/converting-bbcode-into-html-using-csharp?source=rss.

Unable to update the EntitySet because it has a DefiningQuery and no element exists in the element to support the current operation.

$
0
0

After integrating the new forum code, I added basic subscription support. When replying to a topic and opting to subscribe to notifications, the following exception would be thrown:

Unable to update the EntitySet 'ThreadSubscriptions' because it has a DefiningQuery and no element exists in the element to support the current operation.

I'd already checked the Entity model to ensure the relationships were set up correctly as a many to many, as one user may be subscribed to many threads, and any given thread can have many subscribed users, so I was a little perplexed as to where this was coming from.

After looking at the database table which links threads and users, I realized the problem was the table didn't have a unique key, only the relationships. After creating a primary key on the two columns in this table, and regenerating the Entity model, the exception disappeared and subscriptions are now working as expected.

It's always the little things...

All content Copyright (c) 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 https://www.cyotek.com/blog/unable-to-update-the-entityset-because-it-has-a-definingquery-and-no-element-exists-in-the-element-to-support-the-current-operation?source=rss.

Snippet: Mime types and file extensions

$
0
0

If you have a mime type and you want to find the default extension for it, you can get this from the Extension value in the following registry key:

HKEY_CLASSES_ROOT\MIME\Database\Content Type\<mime type>

    public static string GetDefaultExtension(string mimeType)
    {
      string result;
      RegistryKey key;
      object value;

      key = Registry.ClassesRoot.OpenSubKey(@"MIME\Database\Content Type\" + mimeType, false);
      value = key != null ? key.GetValue("Extension", null) : null;
      result = value != null ? value.ToString() : string.Empty;

      return result;
    }

One the other hand, if you have a file extension and you want to know what that mime type is, you can get that via the Content Type value of this key:

HKEY_CLASSES ROOT\<extension>

    public static string GetMimeTypeFromExtension(string extension)
    {
      string result;
      RegistryKey key;
      object value;

      if (!extension.StartsWith("."))
        extension = "." + extension;

      key = Registry.ClassesRoot.OpenSubKey(extension, false);
      value = key != null ? key.GetValue("Content Type", null) : null;
      result = value != null ? value.ToString() : string.Empty;

      return result;
    }

All content Copyright (c) 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 https://www.cyotek.com/blog/mime-types-and-file-extensions?source=rss.

Creating a Windows Forms Label that wraps with C#

$
0
0

One of the few annoyances I occasionally get with C# is the lack of a word wrap facility for the standard Label control.

Instead, if the AutoSize property is set to True, the label will just get wider and wider. In order to wrap it, you have to disable auto resize then manually ensure the height of the label is sufficient.

The base Control class has method named GetPreferredSize which is overridden by derived classes. This method will calculate the size of a control based on a suggested value. By calling this method and overriding the OnTextChanged and OnResize methods, we can very easily create a custom label that automatically wraps and resizes itself vertically to fit its contents.

Paste in the following code into a new Component to have a read-to-run wrappable label.

using System;
using System.ComponentModel;
using System.Drawing;
using System.Windows.Forms;

namespace Cyotek.Windows.Forms
{
  public partial class WrapLabel : Label
  {
		#region  Public Constructors  

    public WrapLabel()
    {
      this.AutoSize = false;
    }

		#endregion  Public Constructors  

		#region  Protected Overridden Methods  

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

      this.FitToContents();
    }

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

      this.FitToContents();
    }

		#endregion  Protected Overridden Methods  

		#region  Protected Virtual Methods  

    protected virtual void FitToContents()
    {
      Size size;

      size = this.GetPreferredSize(new Size(this.Width, 0));

      this.Height = size.Height;
    }

		#endregion  Protected Virtual Methods  

		#region  Public Properties  

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

		#endregion  Public Properties  
  }
}

So, what is the code doing? It's very straightforward.

In the constructor, we are disabling the built in auto resize functionality, otherwise you won't be able to resize the control in the designer.

Next, we want to overide the OnTextChanged and OnResize methods to call our new resize functionality. By overriding these, we can ensure that the control will correctly resize as required.

Now to implement the actual resize functionality. The FitToContents method calls the label's GetPreferredSize method, passing in the width of the control. This method returns a Size structure which is large enough to hold the entire contents of the control. We take the Height of this (but not the width) and apply it to the label to make it resize vertically.

When calling GetPreferredSize, the size we passed in only had the width specified, which will be the maximum width returning. As we passed in zero for the height, the method defines its own maximum height.

Finally, you'll note that we have overridden the AutoSize property itself and added a number of attributes to it to make sure it doesn't appear in any property or code windows, and to prevent its value from being serialized.

All content Copyright (c) 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 https://www.cyotek.com/blog/creating-a-windows-forms-label-that-wraps-with-csharp?source=rss.

Boulder Dash Part 1: Implementing Sprite AI

$
0
0

One of the projects I've had on the backburner for over a year now was a Boulder Dash clone. While I was working on this clone I written a basic game engine using GDI, another using managed DirectX, editing tools, and even a conversion tool for the BDCFF. Everything but the game itself.

After working pretty much nonstop on the Sitemap Creator and WebCopy tools recently, I wanted to take things a bit easy between releases and wanted to resurrect this project.

If you haven't heard of Boulder Dash you're missing out on some classic gaming of yesteryear. Basically, it involved collecting a given number of diamonds in a cave, and there were various enemies (butterflies and fireflies) and game elements (diamonds, boulders, various types of walls, slime, amoeba) which you use to beat each cave. There's lots more than this basic synopsis of course, but it covers the essential elements you will see.

This series of articles will describe some of the design of the game using sample projects to demonstrate the different elements, starting with the AI of the enemies.

In Boulder Dash, enemies don't follow a specific path, nor do they chase you as such. Instead, they are governed by a series of rules.

Firefly Movement Rules

  • if the space to the firefly's left is empty then turn 90 degrees to firefly's left and move one space in this new direction
  • otherwise if the space ahead is empty then move one space forwards
  • otherwise turn to the right, but do not move

This pattern means a firefly can instantly turn left, but takes double the time when turning right.

Butterfly Movement Rules

The butterfly shares the same basic rules as the firefly, the exception being that the directions are reversed. For the butterfly, the preferred turning direction is right rather than left. So the butterfly can instantly turn right, but is slower at moving left.

The sample project

The sample project in action.

The sample project (available to download from the link below) creates a basic testing environment. A map is randomly generated to which you can add fireflies or butterflies. A directional arrow displays the current facing of the sprites. Each second the sprites will be updated.

In this first article we aren't interested in further topics such as collision detection, we just want to make sure our sprites move according to the rules above.

The basic logic for each sprite is:

  • can I move in my preferred direction?
  • can I move straight ahead?

If the answer to either of these questions is "Yes", then our sprite will move. If "No", then it will turn in the opposite direction to its preferred direction.

In Boulder Dash, each cave (level) is comprised of a grid of tiles, nothing fancy. The player can move up, down, left or right, but not diagonally. All other game elements are constrained in the same way.

The following snippet shows the movement logic for the Firefly:

      // first see if we can move in our preferred direction, left
      tile = this.GetAdjacentTile(this.GetNewDirection(Direction.Left));
      if (!tile.Solid)
      {
        // we can move here, update our position and also set our new direction
        this.Location = tile.Location;
        this.Direction = this.GetNewDirection(Direction.Left);
      }
      else
      {
        // can't move in our preferred direction, so lets try the direction the sprite is facing
        tile = this.GetAdjacentTile(this.Direction);
        if (!tile.Solid)
        {
          // we can move here, update our position, but not the direction
          this.Location = tile.Location;
        }
        else
        {
          // can't move forwards either, so finally lets just turn right
          this.Direction = this.GetNewDirection(Direction.Right);
        }

The above code relies on two helper methods, one to return a new direction based on the current direction, and a second to return an adjacent cell from a given direction.

GetNewDirection

The GetNewDirection method below calculates a new direction based on the current sprites direction and a new facing of either left or right.

    public Direction GetNewDirection(Direction turnDirection)
    {
      Direction result;

      switch (turnDirection)
      {
        case Direction.Left:
          result = this.Direction - 1;
          if (result < Direction.Up)
            result = Direction.Right;
          break;
        case Direction.Right:
          result = this.Direction + 1;
          if (result > Direction.Right)
            result = Direction.Up;
          break;
        default:
          throw new ArgumentException();
      }

      return result;
    }

GetAdjacentTile

The GetAdjacentTile method simply returns the text next to the current sprite in a given direction.

    public Tile GetAdjacentTile(Direction direction)
    {
      Tile result;

      switch (direction)
      {
        case Direction.Up:
          result = this.Map.Tiles[this.Location.X, this.Location.Y - 1];
          break;
        case Direction.Left:
          result = this.Map.Tiles[this.Location.X + 1, this.Location.Y];
          break;
        case Direction.Down:
          result = this.Map.Tiles[this.Location.X, this.Location.Y + 1];
          break;
        case Direction.Right:
          result = this.Map.Tiles[this.Location.X - 1, this.Location.Y];
          break;
        default:
          throw new ArgumentException();
      }

      return result;
    }

Once the sample has gotten a tile, it will check to see if the sprite can move into the tile. For our example, we are just using a bit flag to state if the tile is solid or not, but in future we'll need to add collision detection for all manner of game elements.

If the sprite can move into the first tile into it's preferred direction, it will do this. Otherwise, the movement routine will next check to see if the tile in front of the sprite is solid, and if so again it will move. If neither of the two movements were possible then it will update it's current facing to be the opposite of it's preferred direction. The process will be repeated for each "scan" of the game elements.

Using these rules it is quite easy to setup scenarios where the sprites can "guard" a game element by endless circling it. And just as easily the unwary player will be chased mercilessly if they are unwary.

Please let us know if you'd like to see more of this type of article here on cyotek!

Edit 07/07/2010: Please also see Boulder Dash Part 2: Collision Detection

Downloads

All content Copyright (c) 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 https://www.cyotek.com/blog/boulderdash-part-1-implementing-sprite-ai?source=rss.

Creating a Windows Forms RadioButton that supports the double click event

$
0
0

Another of the peculiarities of Windows Forms is that the RadioButton control doesn't support double clicking. Granted, it is not often you require the functionality but it's a little odd it's not supported.

As an example, one of our earlier products which never made it to production uses a popup dialog to select a zoom level for a richtext box. Common zoom levels are provided via a list of radio buttons. Rather than the user having to first click a zoom level and then click the OK button, we wanted the user to be able to simply double click an option to have it selected and the dialog close.

However, once again with a simple bit of overriding magic we can enable this functionality.

Create a new component and paste in the code below (using and namespace statements omitted for clarity).

  public partial class RadioButton : System.Windows.Forms.RadioButton
  {
    public RadioButton()
    {
      InitializeComponent();

      this.SetStyle(ControlStyles.StandardClick | ControlStyles.StandardDoubleClick, true);
    }

    [EditorBrowsable(EditorBrowsableState.Always), Browsable(true)]
    public new event MouseEventHandler MouseDoubleClick;

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

      // raise the event
      if (this.MouseDoubleClick != null)
        this.MouseDoubleClick(this, e);
    }
  }

This new component inherits from the standard RadioButton control and unlocks the functionality we need.

The first thing we do in the constructor is to modify the components ControlStyles to enable the StandardDoubleClick style. At the same time we also set the StandardClick style as the MSDN documentation states that StandardDoubleClick will be ignored if StandardClick is not set.

As you can't override an event, we declare a new version of the MouseDoubleClick event using the new keyword. To this new definition we add the EditorBrowsable and Browsable attributes so that the event appears in the IDE property inspectors and intellisense.

Finally, we override the OnMouseDoubleClick method and invoke the MouseDoubleClick event whenever this method is called.

And there we have it. Three short steps and we now have a radio button that you can double click.

All content Copyright (c) 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 https://www.cyotek.com/blog/creating-a-windows-forms-radiobutton-that-supports-the-double-click-event?source=rss.

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

public bool 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
{
  public override void Move()
  {
    // Do nothing, this sprite doesn't automatically move
  }

  public override 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:

public abstract 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:

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

And the one for EnemySprite is almost as easy:

public override CollisionAction GetCollisionAction(Sprite collidedWith)
{
  CollisionAction result;

   if (collidedWith is PlayerSprite)
    result = CollisionAction.Explode; // Kill player
  else
    result = CollisionAction.None; // Do nothing

  return 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 action
if (collision != null && this.GetCollisionAction(collision)== CollisionAction.Explode)
{
  // kill both this sprite and the one we collided with
  this.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 (c) 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 https://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.

protected override 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.

public void SetHorizontalExtent()
{
  int maxWith;

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

    textSize = TextRenderer.MeasureText(item.ToString(), this.Font);
    if (textSize.Width > maxWith)
      maxWith = textSize.Width;
  }

  this.SetHorizontalExtent(maxWith);
}

public void 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:

private const int WS_HSCROLL = 0x100000;
private const int CB_SETHORIZONTALEXTENT = 0x015E;

[DllImport("user32.dll")]
private static extern 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 (c) 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 https://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.

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

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

  cellSize = (int)(cellSize * scale);

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

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

  return result;
}

Painting the control

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

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

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

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

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

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

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

Sample Project

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

Downloads

All content Copyright (c) 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 https://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)]
public new Size AutoScrollMainSize
{
  get { return base.AutoScrollMinSize; }
  set { base.AutoScrollMinSize = value; }
}

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

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

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

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

Reacting to scroll changes

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

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

  base.OnScroll(se);
}

Painting adjustments

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

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

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

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

  borderOffset = this.GetBorderOffset();

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

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

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

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

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

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

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

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

Sample Project

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

Downloads

All content Copyright (c) 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 https://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.

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

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

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

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

      position = new Point(x, y);

      this.UpdateScrollPosition(position);
    }
  }
}

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

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

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

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

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

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

Keyboard Scrolling

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

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

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

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

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

  return result;
}

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

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

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

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

  this.UpdateScrollPosition(scrollPosition);
}

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

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

Sample Project

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

Downloads

All content Copyright (c) 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 https://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.

private static readonly int MinZoom = 10;
private static readonly int MaxZoom = 3500;

[DefaultValue(100), Category("Appearance")]
public int Zoom
{
  get { return _zoom; }
  set
  {
    if (value < ImageBox.MinZoom)
      value = ImageBox.MinZoom;
    else if (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.

protected virtual int ScaledImageHeight
{ get { return this.Image != null ? (int)(this.Image.Size.Height * this.ZoomFactor) : 0; } }

protected virtual int ScaledImageWidth
{ get { return this.Image != null ? (int)(this.Image.Size.Width * this.ZoomFactor) : 0; } }

protected virtual double 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.

protected virtual void AdjustLayout()
{
  if (this.AutoSize)
    this.AdjustSize();
  else if (this.SizeToFit)
    this.ZoomToFit();
  else if (this.AutoScroll)
    this.AdjustViewPort();
  this.Invalidate();
}

public virtual void 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.

public virtual 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()
{
  return this.GetInsideViewPort(false);
}

public virtual 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;
  }

  return new Rectangle(left, top, width, height);
}

public virtual 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 borders
  switch (this.BorderStyle)
  {
    case BorderStyle.FixedSingle:
      ControlPaint.DrawBorder(e.Graphics, this.ClientRectangle, this.ForeColor, ButtonBorderStyle.Solid);
      break;
    case BorderStyle.Fixed3D:
      ControlPaint.DrawBorder3D(e.Graphics, this.ClientRectangle, Border3DStyle.Sunken);
      break;
  }

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 background
  using (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.

protected override void 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.

protected override void 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;
      else if (this.Zoom >= 75)
        this.Zoom = 100;
      else
        this.Zoom = (int)(this.Zoom / 0.75F);
    }
    else if (e.Button == MouseButtons.Right || (e.Button == MouseButtons.Left && Control.ModifierKeys != Keys.None))
    {
      if (this.Zoom > 100 && this.Zoom <= 125)
        this.Zoom = 100;
      else if (this.Zoom > 100)
        this.Zoom = (int)Math.Round((double)(this.Zoom - 100) / 100) * 100;
      else
        this.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 (c) 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 https://www.cyotek.com/blog/creating-a-scrollable-and-zoomable-image-viewer-in-csharp-part-4?source=rss.

Viewing all 559 articles
Browse latest View live


Latest Images