Cropping images meets cross hairs - arcdev/engram404 GitHub Wiki

originally posted 2016-04-13 at https://engram404.net/cropping-images-meets-cross-hairs/

Of late, I've found myself scanning a bunch of documents and images that need to be cropped.  One thing I struggle with each time is being able to see the edges I'm working with especially if the image is ever-so-slightly skewed.  I end up starting at a corner (let's say top-left), then when I start drawing the box to crop/select the opposing corner is beautiful because I can see by the selection box exactly what'll be cropped.  Then, I end up seeing that I cut of part of the top or left side.  So I have to start the selection, again.  That feels wasteful.

It occurred to me that a perfect solution would be to have the selection cross-hairs extend to the whole image, window, or screen so I started looking for options.

Since most of the cropping/selecting I do it in my favorite image editing tool, Paint.NET, I thought I'd look to see if there was already an plugin that would change the cursor.  Nope – none that I could find.  Since Paint.NET has an API for developing your own plugin, I started poking around there.  No luck.  It seems the plugins are only for effects, not for new tools.

Next was casting about looking for a good tool that was already written.  Maybe something that would paint a vertical and horizontal line on the screen over anything.  I did find one my Michael Lin from 2007 called CrossHairs v1.1.  While a good start, it seemed a bit limited (no source) and I'm not terribly happy with shortcut keys and no visible UI for things like this.  (I get this reasoning, but…)  You can download his from here.

I found someone created an AutoHotKey script to do just this.  And while I love AutoHotKey, it just felt like a hack… and I'm not sure why.

Beyond that, I didn't really find much else.  Most of what I found was for gamers and not really relevant.

(Note: I did find a couple neat implementations for web sites, like this one, but that didn't really solve my problem.)

The next thing that cross my mind was to write an image cropping app, but that seemed like overkill.  Plus, it seemed that a full overlay set of cross hairs could be useful for other things (like checking alignments on web layouts and such).

The AutoHotKey (AHK) script seemed like the way to go (with a few tweaks), since I always have AHK running anyway.

This got me thinking if I could roll my own.

One of the first things I did was simply try to change the system's mouse cursor to something huge – big enough to extend beyond the screen boundaries.  I found a StackOverflow post (from endofzero) that seemed promising, but it didn't work for me.  (I might come back to it later as it certainly seems like a neat idea.)  Regardless, it was fun to download an emoji  image  and set it to the system cursor (using LoadImage and SetSystemCursor).

Next, I started with a basic WinForms app in C# and tried to coerce it into what I wanted.  At this point, though, I can't even tell you all of the iterations and dead ends I went down.

I managed to make my form transparent, but then lost the mouse movements events.  After casting about, I found this great post on capturing low level events.  That got me started with the keyboard and I expanded on it to make the mouse events work.

It's worth noting that only a couple Windows Hook messages can be captured in managed code [MSDN]

Except for the WH_KEYBOARD_LL low-level hook and the WH_MOUSE_LL low-level hook, you cannot implement global hooks in the Microsoft .NET Framework. To install a global hook, a hook must have a native DLL export to inject itself in another process that requires a valid, consistent function to call into. This behavior requires a DLL export. The .NET Framework does not support DLL exports. Managed code has no concept of a consistent value for a function pointer because these function pointers are proxies that are built dynamically.

Low-level hook procedures are called on the thread that installed the hook. Low-level hooks do not require that the hook procedure be implemented in a DLL.

That's fine because I could work with WH_MOUSE_LL.

(BTW – this stuff was a throwback for me.  I used to play with this back in the early 2000's using VB6.  I remember having a big, weighty tome of a reference all on the Win32 APIs that I picked up at Half Price Books.  Kinda made me shudder, but… onward and upward!)

Once I got the mouse move events captured, and at the system level no doubt, I thought maybe I could just paint right on the desktop.  That turned ugly having to figure out exactly what areas to mark as invalidated at any given time.  (More on that later, it turns out.)

Giving up on working without a net, er form, I went back to my WinForm.

Drawing on the form itself was no big deal (thanks to the Form.OnPaint event) using simple Graphics.DrawLine method calls.  Unfortunately, clearing the old lines wasn't so easy.

Every time the mouse moved, I called Form.Invalidate which cleared the previous drawing and caused OnPaint to be called again.  But, that somehow ate up all of the mouse click events, too.  Ugh.

So, I figured that I'd go look at what the AHK script was doing.  After a lot of digging through the script, pulling the AHK source, and trying to remember how to read C++, I realized what they were doing.  They were actually creating two forms, setting a bunch of styles, and moving the forms about.  One form was rendered in such a way as to look like the vertical line, and the other as the horizontal.  That kinda felt like cheating, but… I'm game.

I tried the same thing with pure, well mostly pure (I still needed the WM_MOUSE_LL hook), .NET forms.  For whatever reason, I couldn't get my forms to be any less than 2 pixels wide. '"That works!'" you might say.  But, no, not for me.  I wanted 1 pixel.  '"But, why?'" you ask.  Because otherwise, the solution isn't any better than the AHK one.  Sigh… moving on…

Since AHK uses C++, I opened my browser to the trusty pinvoke.net.  Ah, the beauty and clutter of well-meaning, but disorganized developers.  Seriously, it's a fantastic resource – especially when combined with MSDN.  I took all of this and started creating a native Win32 API calls from C# to register a new [window] class and create it.  [sidebar: check out my upcoming post A Complete PInvoke Wrapper for .NET] Except….every time I tried to create the class the program unceremoniously crashed.  Ok… not gonna fight with that.  Let's try something else…

Back to invalidating regions.

It occurred to me that the blocking of the click events might have something to do with using the '"big hammer'" (aka invalidating the entire form).  I opted to invalidate only part of the form.  Viola!  That worked!  It wasn't a pretty painting, but it worked.  I extended that idea and invalidated the regions above, below, to the left, and to the right of the Form.MousePosition.

Well, sorta…

It still seemed that the painting wasn't quite right and that the clicks didn't always make their way through to the underlying windows.  I decided to create a gap around the MousePosition – basically, a dead zone, where I never invalidated.  That was the key.

Of course, now that I'm invalidating regions, I'm curious to go back to drawing directly on the desktop instead of needing a transparent form.  (Incidentally, check out my upcoming post Creating a Transparent Form.)

[update 2016.04.14]

I tried the direct draw on the desktop, but the issue appears to be that when any other window is invalidated the lines that we drew get erased.  That's not gonna help much… Back to cleaning up my TopMost, Transparent form…

Once I get the code cleaned up a bit, I'll post it to GitHub and add the link here.  Stay tuned...

Source code! CrossHairOverlay