Home > Mobile >  GetClientRect is not giving the correct Rectangle
GetClientRect is not giving the correct Rectangle

Time:01-31

I am trying to create an overlay form that overlays the contents of an external window (excluding the borders etc). I believe that GetClientRect is the correct winapi for that purpose however it does not seem to be working.

I created an example where I load up a form as a black box and display it over an open Notepad.

using System.Runtime.InteropServices;
using System.Diagnostics;

namespace WinFormsApp2
{

    public partial class Form1 : Form
    {
        private Process? targetProc = null;

        public struct RECT
        {
            public int Left;
            public int Top;
            public int Right;
            public int Bottom;
        }

        [DllImport("user32.dll", SetLastError = true)]
        public static extern bool GetClientRect(IntPtr hwnd, out RECT lpRect);

        [DllImport("user32.dll")]
        public static extern bool ClientToScreen(IntPtr hWnd, ref Point lpPoint);

        public static Rectangle GetWindowRectangle(IntPtr hWnd)
        {
            Point point = new Point();
            GetClientRect(hWnd, out RECT rect);
            ClientToScreen(hWnd, ref point);
            return new Rectangle(point.X, point.Y, rect.Right, rect.Bottom);
        }

        private IntPtr notepadhWnd;

        public Form1()
        {
            InitializeComponent();

            this.FormBorderStyle = FormBorderStyle.None;
            this.WindowState = FormWindowState.Normal;
            this.BackColor = Color.Black;
            StartPosition = FormStartPosition.Manual;
            targetProc = Process.GetProcessesByName("notepad").FirstOrDefault(p => p != null);

            try
            {
                if (targetProc != null)
                {
                    notepadhWnd = targetProc.MainWindowHandle;

                    if (notepadhWnd != IntPtr.Zero)
                    {
                        Rectangle rect = GetWindowRectangle(notepadhWnd);
                        Location = new Point(rect.Left, rect.Top);
                        Size = new Size(rect.Width, rect.Height);
                    }
                }
            }
            catch (Exception ex)
            {
                // Log and message or
                throw;
            }
        }

    }
}

The output of this is: Notepad being completely covered by form

I expected the output to be: Only contents of Notepad being covered by form

From all my searches I believe that the GetClientRect API should only return the client area but not sure what I am missing.

CodePudding user response:

Grab coffee, this will be a long answer.

First, take a look at the image of notepad below. From the wording of your question it sounds like you are expecting GetClientRect to return the area marked in red. GetClientRect returns the dimensions of the window handle you provide but without the borders or scroll bars. In other words, it will give you the interior area of the green rectangle.

Notepad with rectangles

There is no single "client window" - in the screenshot below the menu bar is a child window. So is the status bar at the bottom of the screen. So is the editor space which seems to be what you are interested in.

So, how do you get the hWnd of the editor space? I'm not aware of any answer to that question that doesn't rely on the dark magic. It is all fraught with peril... do you iterate through all the child windows and pick the biggest? Pick a point in the dead center of the window and use API calls to find the hWnd at that location? No matter what you do, it will not be an exact science. For purposes of this question, though, I'll show one approach.

Every window has a Class name associated with it. In the image below I'm using a tool called Spy to reveal information about the various windows. As you can see, the window that makes up the editor space has a class name of "RichEditD2DPT"

enter image description here

I want to stress again how brittle this solution is. You could have more than one child window of the given class. The application developer could change to a different control with a different class name without warning. Nevertheless, below is code to enumerate through the child windows until the desired class is found. After that, I'm simply passing that hWnd into the code you already had and it seems to work.

public partial class Form1 : Form
{
    private readonly StringBuilder _buffer = new StringBuilder(1024);
    private IntPtr _notepadMainhWnd;
    private IntPtr _notepadEditorhWnd;

    public struct RECT
    {
        public int Left;
        public int Top;
        public int Right;
        public int Bottom;
    }

    [DllImport("user32.dll", SetLastError = true)]
    [return: MarshalAs(UnmanagedType.Bool)]
    public static extern bool GetClientRect(IntPtr hwnd, out RECT lpRect);

    [DllImport("user32.dll")]
    [return: MarshalAs(UnmanagedType.Bool)]
    public static extern bool ClientToScreen(IntPtr hWnd, ref Point lpPoint);

    [DllImport("user32.dll")]
    [return: MarshalAs(UnmanagedType.Bool)]
    private static extern bool EnumChildWindows(IntPtr hwnd, WindowEnumProc func, IntPtr lParam);

    [DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Auto)]
    private static extern int GetClassName(IntPtr hWnd, StringBuilder lpClassName, int nMaxCount);

    private delegate bool WindowEnumProc(IntPtr hwnd, IntPtr lparam);

    public static Rectangle GetWindowRectangle(IntPtr hWnd)
    {
        Point point = new Point();
        GetClientRect(hWnd, out RECT rect);
        bool ret = ClientToScreen(hWnd, ref point);
        Debug.WriteLine($"{point.X},{point.Y}");
        return new Rectangle(point.X, point.Y, rect.Right - rect.Left, rect.Bottom - rect.Top);
    }

    public Form1()
    {
        InitializeComponent();

        this.FormBorderStyle = FormBorderStyle.None;
        this.WindowState = FormWindowState.Normal;
        this.BackColor = Color.Black;
        StartPosition = FormStartPosition.Manual;
        var targetProcess = Process.GetProcessesByName("notepad").FirstOrDefault(p => p != null);

        if (targetProcess != null)
        {
            // Get the main application window
            _notepadMainhWnd = targetProcess.MainWindowHandle;

            if (_notepadMainhWnd != IntPtr.Zero)
            {
                // Looks for child windows having the class name "RichEditD2DPT"
                EnumChildWindows(_notepadMainhWnd, callback, IntPtr.Zero);
                if (_notepadEditorhWnd != IntPtr.Zero)
                {
                    Rectangle rect = GetWindowRectangle(_notepadEditorhWnd);
                    Location = new Point(rect.Left, rect.Top);
                    Size = new Size(rect.Width, rect.Height);
                }
            }
        }
    }

    private bool callback(IntPtr hWnd, IntPtr lParam)
    {
        if (GetClassName(hWnd, _buffer, _buffer.Capacity) != 0)
        {
            if (string.CompareOrdinal(_buffer.ToString(), "RichEditD2DPT") == 0)
            {
                _notepadEditorhWnd = hWnd;
                return false;
            }
        }

        return true;
    }
}
  • Related