First Approach: Windowed Application
Second Long Approach – C# and GDI+
Prior Experience Expectation
When the desktop is visible display in the upper right hand corner the Host name and IP address at a minimum.
If the application is provided additional input at launch: run a command, or script capture the output and display that additional information.
Display all information without modifying the background image causing skewing, scaling, or changing the wallpaper mode.
Evolution of solution: WinGDI+ Active Window
I approached it with the idea in mind that I want this to show up when a user presses Win+D or Alt+Tab, or based on the “active window.” I was thinking too much about the windows. I added more code using “User32.dll” to get the active window handle and then that window’s Title. With some if-statements I was able to get some basic functionality where I could force it to draw only when the active window was null, task switcher, start menu/Cortana, or an empty string.
This was a good starting point but I was running into ugliness when the text flickered during each loop or would draw over some windows, and would randomly disappear when user pressed Win+D and then clicked on the desktop
Hooks on Active Windows
I started working with Windows Hooks. I used more “user32.dll” methods to create hooks, read windows event messages, and strengthen my main Loop. I added a .NET stop watch to the main loop, replacing a wait method, allowing redraw after a specific delta but allowing the loop to process windows messages. I implemented a Windows hook with a delegate to an overloaded Redraw method of the Overlay object. Getting the right windows events hooked was tricky – I ended up hooking to: SwitchStart, MinimizeStart, and Foreground (window changed). This was almost good enough.
At this point my program would…
- Draw when the user Alt+Tab’d, Win+D, Opened start menu, clicked on the Desktop, or any Window with empty string for Window Name. The flicker was minimized although certain conditions made it flicker more than I wanted.
A Ray of game dev light hit me:
When I got back to my desk my code for Overlay was still up – Thinking about my wants for the program it finally occurred to me the solution I had been missing all along.
First the 'want'
I was currently checking for whatever the active window was not caring about where it was on the screen or if it was obstructing the overlay draw area. In game dev I can drop a ray and find what it intersects with why would this be any different – surely there is a way to know what window is under the four vertices of my draw area?
User32.dll has a method for that: IntPtr WindowFromPoint(Point point) - why did I not think of this. Time wasted on getting the delegate in the correct hooks in windows events - Arrrgh.
So initially I added to all my existing code (keeping the event hooks) a check in the main program loop to see if there was a window over my Overlay area… if not redraw. This worked! I then did some more tuning to how I managed the list of window titles that I was allowing the program to draw over preparing to add them to a config file that could be easily modified in the future. I added code to work up the handles of objects over the overlay’s to get the root parent window because at times applications in Windowed (not full screen) mode were overlapping the overlay area were returning a handle with a window title but a null string unless placed very specifically. I could now drag windows over the overlay area and it would get cleared out by the dragging window’s redraw and then when the window left the overlay area would redraw my overlay text. It worked great.
Final evolution in C#
Re-write in C++
Seeing as most of the workload portions of the application were using native windows GDI+ and User32 .dll's it was an easy task converting the functionality from C# .NET managed code to unmanaged lighter weight C++. I wanted to also be careful that whatever I did I didn’t have any dependencies on .NET or having a particular C++ redistributable installed. Some compile time settings took care of the C++ redistributable dependencies. Alternative implementations of some methods and classes avoided the .NET dependency. I wrote a quick and simple stop watch class to get timers back in my main loop. By limiting the number of times it is able to evaluate if there is a window over the Overlay area and if it should redraw I can throttle the amount of CPU used and limit any redraw flicker.
The first new addition to the program during conversion was adding logic to make the program recalculate the draw area every few seconds so that if the display dimensions changed the overlay would adjust to the new proper location.
Tested on Windows 7 laptop – Nothing
The program was running without crashing or errors but nothing was displaying on screen.
Installed Visual Studio on my Windows 7 laptop and started doing compiling there – nothing. While debugging I found that the overlay position was being calculated incorrectly due to the display topology… easy fix but that didn’t explain the VM’s. I installed remote debugger on a VM and debugged from my main Win 10 workstation – all the variables were correct, the return from the GDI+ DrawString() method was successful. After some searching I found that the way the display driver was configured on the VM, the lack of resources available for Video, and not having VMware tools installed was to blame.
Seeing the same behavior – No Errors, Variables are showing the correct draw locations, and return from DrawString() states success. While getting frustrated and going back and forth to get the resolution test different resolutions I eventually realized that this was the first windows 10 machine that had DPI scaling turned on. I was aware of DPI scaling but was unaware of the way Microsoft had implemented a form of DPI Scaling emulation for applications that don’t tell Windows they are High DPI-aware.
I had converted the program to use a Command Prompt window to display the state of variables while the program ran (instead of using breakpoints which is difficult when you are trying to measure the intersecting windows and such). I could see where the Overlay was supposed to be, when it was supposed to be drawing, that the draws were successful, and when there was a window covering the overlay space. Everything looked right in the application but it wasn’t looking right on the screen – nothing was displaying. I thought that maybe due to the scaling the overlay was off the screen somewhere and added some test code to make the overlay move 10 pixels down and left every few seconds. After running the program and waiting I saw that the overlay appeared. It took me two more restarts to notice that it was only drawing inside of this smaller rectangle originating from the bottom left hand corner of the monitor (thinking back I believe it was bottom right because my debug information that displays in top left was not displaying either). After doing some measuring I was able to conclude that it was drawing inside a RECT that matched the resolution of a DPI scaling of the native resolution. Somehow DPI scaling was limiting the drawing to this emulated resolution.
I worked with trying to find a way to fake the emulated desktop draw area… I think the issue was I was pulling the handle for the “Desktop” window to make the GDI+ DrawString() call and Windows was treating the area I wanted to draw out of bounds of the DPI Scaled “Desktop” window and not drawing.
I started reading more into DPI scaling in Windows 8 and 10 – more than the skimming I did before – and found there was a method that could be called in a thread that would tell windows to treat the process as “HighDPI” aware. This worked on Windows 10 but does not on Windows 7. I added code in to determine the OS at runtime and skip any calls to the function but it was no good. I did not want to have two versions of my application one for Win 7 and older and one for Win 8 and newer. While looking through compile options hoping there was a way I could bundle the required win10 resource into my application or have some sort of runtime ignore calling the function I just happened to notice answer:
Manifest Tool > Input and Output > DPI Awareness
Creates Overlay object
- Reads in arguments to see if Debug is one of them
- Else treats the launch arguments as a Console command and arguments for that command
- Runs the Command + arguments - gets the console output from that command feeds it into the Overlay object as additional text.
- Calls the Run method of the Overlay object.
- Contains a main loop with switch statements that evaluate what action should be performed.
- Each loop calls method to determines if there is a window above the Overlay area but the draw method is only able to be called at most once every 250 ms.
- There is a wait at the end of the loop for 30 ms to limit CPU. CPU usage is less than 2% while drawing and less than 1% while not drawing. Because of the implementation of the stopwatch class when the overlay area is exposed there should be at most 30ms delay before ReDraw() can be called.
- The method call for Overlay Location Recalculation is on a separate stopwatch limiting call to every 4500 ms.