1

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();
    }

}

Which works fine: Manual Drawing

However, I need to be able to draw up here on the title bar:

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.

ScottishTapWater
  • 3,656
  • 4
  • 38
  • 81
  • The best approach to this is probably the second answer listed here: https://stackoverflow.com/questions/2232727/win32-how-to-draw-outside-my-window – gplumb Feb 20 '18 at 22:13
  • You could keep that hooked window and override its CreateParams like this: `CreateParams cp = base.CreateParams; cp.ExStyle = (cp.ExStyle | WS_EX_LAYERED | WS_EX_TRANSPARENT | WS_EX_TOPMOST); return cp;`. When you get the size of the hooked window with `GetWindowRect(hWnd);` you can position your overlay on the title bar, using only its Paint event to draw the string. The size of your overlay can be set by measuring the string to draw with TextRenderer before showing it. – Jimi Feb 21 '18 at 19:09
  • Use `Form.ClientRectangle`, not `GetWindowRect` in user32. And also use `WM_NCPAINT` in `WndProc`. – Muaath Dec 20 '20 at 06:43

0 Answers0