I was presented with a problem where the organization wanted to display some simple text information on the desktop like what Microsoft's Sysinternals BGInfo provides. BGInfo is old and has not been updated to support DPI scaling on High Resolution displays and the organization had not approved the free ware available so as a side project I began developing my own Informational Desktop Overlay... This is a post about my experiences and development process.
First Approach: Windowed Application
On first instinct I thought maybe – I could make a Windowed application that was borderless and had a transparent background with a text box. I soon realized that I could not find a way to keep that window above the desktop when a user would press Win+D to show the desktop. I attempted a few different methods but it seems like I could not restore the window until the user manually restored the application – there is likely a way but I did not find it in my early discovery.
Second Long Approach – C# and GDI+
Prior Experience Expectation
I knew I could draw above everything if I used GDI – albeit if a window underneath the drawing redrew I would lose the GDI drawing. I wrote a “tetris” like game called "Stacked" for a Game Development/Data Structures Class in C++ (can see a version of the game re-written for Android here). The project guidelines intended for us to make a game that ran in Windows Command Prompt Console – so I did – in a way. The menus were all based in Command prompt but when the game started it used GDI calls to draw the board, score, and objects on top of the desktop. Input was received through the command prompt so the command prompt had to maintain focus to play the game. The game would flicker and get slow the more it had to draw on the screen but hey it was pretty slick for a first year game development course.
Program requirements
Some of these were added via scope-creep but in the end this is what the application goals were:
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.
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 chose to work with C# as it is the language I use on a near day to day basis for the last 4 years. I did my research and was soon able to start drawing with GDI+ on the screen (the easiest part). The tricky part was finding a way to make sure I don’t draw over other windows. I was too close to the program and not thinking like a game developer but more like some business application data-manipulation developer (which is what I had been doing exclusively in programming for the last several months – not game dev).
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
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 futzing about more looking for ways to tell when an active window changed so I could redraw less frequently and not only when it triggered in my main program loop. This phase of development led to lots of code that I ended up ripping out but also allowed me to really strengthen up my methods that would last.
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.
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:
I finished the other application about 8 hours of coding and testing – A command prompt application with a variety of command line switches. It was a simple program, not very interesting and did not require much research but had a lot of little nuances and branching conditions.
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.
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'
APPEAR to draw over the Desktop wallpaper ALWAYS when not obstructed.
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.
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#
Implemented the .NET settings file to have a list of all the window names I will allow the Overlay to draw over. I added handling of a “debug” parameter to my launch arguments so that I could force it to write the name of any window that was covering the overlay so I could add it to the Config file. With the Debugging running I noticed a few oddities that I traced back to the Redraw’s called from the Windows Event hooks. I commented out the creation of the hooks, the Overloaded Redraw, some extra variables and had a fairly good overlay…. But bloated by .NET and C#.
Re-write in C++
The performance of .NET and C# were not great using 8 MB of RAM constantly. For a program that is expected to run 100% of the time and only display information sometimes, I was unhappy with the resources required.
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.
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.
Display issues
After I had the program completed and working on my Windows 10 desktop I started to make sure it would work on Windows 7 and Windows 10 computers without Dev Tools. I had some Windows 10 imaged VM’s with minimal specs on an ESXi host and tested my application.
Nothing.
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.
Nothing.
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.
Debugging
To continue my application testing I installed a copy of my customized image of Windows 10 on a newish test laptop I had available and tested my application… Nothing – no drawing. What?! I thought this was figured out! Installed remote debugger and started looking into the cause.
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.
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
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
After cleaning up my earlier attempts at getting around DPI scaling, by just adding this setting the application now worked properly on Windows 7, Windows 10 without scaling, and Windows 10 with DPI scaling.
Code Synopsis
Main()
Creates Overlay object
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.