I am attempting to overlay a status message on an external application.
Previously, I achieved this by using a TransparencyKey
on a Form
and the following API call to get the window location with a hook
to capture the window moved event.
[DllImport("user32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool GetWindowRect(IntPtr hWnd, ref Rect lpRect);
This works on my development machine, however, on the target machine it fails, the transparent area actually has hatch markings over it. Presumably something to do with that machine being a VM and having no hardware graphics accelleration.
So, I've done away with the Form
and have the following code (with DrawFrame()
being called every 60ms) to draw manually to the screen:
class DirectDisplay:IDisposable
{
private Graphics window;
private SolidBrush b;
private string Status;
private NativeMethods.Rect location;
private bool running;
public DirectDisplay(IntPtr _targetHWnd, string status)
{
Status = status;
b = new SolidBrush(Color.FromArgb(255,0,149,48));
window = Graphics.FromHwnd(_targetHWnd);
}
public void DrawFrame()
{
window.DrawString(Status, new Font("Arial", 12),b, 0, -20);
}
public void Dispose()
{
window?.Dispose();
b?.Dispose();
}
}
However, I need to be able to draw up here on the title bar:
It appears as though utilising
Graphics.FromHwnd()
limits me to using the typeable space in notepad
so I can't draw directly into the button bar.
How do I get a graphics object encompassing the entire Window that I can draw over?
For reference, here is the rest of my code:
namespace OnScreenOverlay
{
class DataManager:IDisposable
{
private const string USER_CACHE = @"C:\test.txt";
private DirectDisplay directDisplay;
private volatile bool exiting;
private readonly Process _target;
private readonly IntPtr _targetHWnd;
private string _currentUser;
private int _daysUntilExpiry;
private NativeMethods.Rect _location;
public DataManager()
{
_target= Process.GetProcessesByName("notepad")[0];
if (_target== null)
{
MessageBox.Show("No target detected... Closing");
}
_targetHWnd = _target.MainWindowHandle;
//InitializeWinHook();
GetCurrentUser();
GetExpiryDate();
directDisplay = new DirectDisplay(_targetHWnd, $"Current User: {_currentUser} --- Password Expires: {_daysUntilExpiry} days");
}
private void GetCurrentUser()
{
if (File.Exists(USER_CACHE))
{
_currentUser = File.ReadAllLines(USER_CACHE)[0].Split('=')[0];
}
else
{
Application.Exit();
}
}
private void GetExpiryDate()
{
using (PrincipalContext domain = new PrincipalContext(ContextType.Domain))
{
using (UserPrincipal user = UserPrincipal.FindByIdentity(domain, IdentityType.SamAccountName, _currentUser))
{
DateTime? pwLastSet = user?.LastPasswordSet;
if (pwLastSet.HasValue)
{
_daysUntilExpiry = (int)(TimeSpan.FromDays(7) - (DateTime.Now - pwLastSet.Value)).TotalDays;
}
else
{
_daysUntilExpiry = int.MinValue;
}
}
}
}
public void Start()
{
while (!exiting)
{
directDisplay.DrawFrame();
Thread.Sleep(1000/60); //Prevent method returning
}
}
private void InitializeWinHook()
{
NativeMethods.SetWinEventHook(NativeMethods.EVENT_OBJECT_LOCATIONCHANGE, NativeMethods.EVENT_OBJECT_LOCATIONCHANGE, IntPtr.Zero, TargetMoved, (uint)_target.Id,
NativeMethods.GetWindowThreadProcessId(_target.MainWindowHandle, IntPtr.Zero), NativeMethods.WINEVENT_OUTOFCONTEXT | NativeMethods.WINEVENT_SKIPOWNPROCESS | NativeMethods.WINEVENT_SKIPOWNTHREAD);
}
private void TargetMoved(IntPtr hWinEventHook, uint eventType, IntPtr lParam, int idObject, int idChild, uint dwEventThread, uint dwmsEventTime)
{
NativeMethods.GetWindowRect(_targetHWnd, ref _location);
}
public void Dispose()
{
directDisplay?.Dispose();
_target?.Dispose();
}
}
}
I realise that I don't need the WindowsHook stuff if I'm utilising this method to get the Graphics
object, I just haven't removed it yet in-case this method doesn't work.
Further to this, I know I could get the Graphics
object for the Desktop using IntPtr.Zero
but I only want my overlay to be a Single Z-level above the target application.