take a screenshot and visually highlight the focus

2019-06-06 15:15发布

问题:

After happily using open source software for many years I've figured it's time to give back. And since documentation is usually a weak point for many projects, plus my C# skills are not exactly in high demand in my corner of FLOSS land, I've figured I'll start with tutorials, etc.

After the second tutorial for a burning tool, I already got annoyed with the routine of

  1. take screenshot
  2. highlight any important part
  3. annotate it
  4. add to website
  5. repeat

and figured I could automate that.

I guess what I'm looking for is a program that would take a screenshot of the currently open window, paint e.g. a yellow bar around the focused control (a button maybe) then pop up a little textbox for me to enter a description for the picture and finally add it all to a website/database/list/etc.

Now for my actual question: Unless anybody knows a tool that already does that, I need some starters on how to access size and position of controls on 'foreign' windows so that I can calculate where to paint those highlighting bars around important controls. I remember those password-unmasking tools for Windows that could reveal the content of any ******-protected textbox, but I can't find any open examples on that.. WinAPI stuff I guess, WindowFromPoint + GetDlgItem or something like that. No idea if it's any easier in Linux, either one will be fine though. No preference on programming language either.

回答1:

To my knowledge, what you want to do requires some P/Invoke, since .NET does not feature any API for accessing the windows of other applications.

You could probably start by using GetForegroundWindow to get the current Window (you would need to fire that code using a global hotkey or a timer, because if you switch windows to take the screenshot, you will get your own window returned from GetForegroundWindow).

Edit

I got inspired by your question to do a little Sunday afternoon coding. I found out, that GetForegroundWindow will get you the foreground Window, but not a Control level. But there is another useful function, GetGUIThreadInfo, that will get you the currently focused control and some other info. We can use GetWindowInfo to get the information about the Window (which might be a control contained in a top-level window).

Putting these things together, we can make a Window class that abstracts away all the gritty P/Invoke calls:

using System;
using System.Drawing;
using System.Runtime.InteropServices;
using System.Text;

namespace dr.Stackoverflow.ScreenshotTest
{
    public class Window
    {
        private WINDOWINFO info;
        private readonly IntPtr handle;

        internal Window(IntPtr handle)
        {
            this.handle = handle;
        }

        public int Handle
        {
            get { return handle.ToInt32(); }
        }

        // Note - will not work on controls in other processes.
        public string Text
        {
            get
            {
                int length = GetWindowTextLength(handle);
                if ( length > 0 )
                {
                    StringBuilder buffer = new StringBuilder(length);
                    if (0 < GetWindowText(handle, buffer, length))
                    {
                        return buffer.ToString();
                    }
                }
                return "<unknown>";
            }
        }

        public Rectangle WindowArea
        {
            get
            {
                EnsureWindowInfo();
                return info.rcWindow;
            }
        }

        public override string ToString()
        {
            return String.Format("{0} 0x{1}", Text, handle.ToString("x8"));
        }

        private unsafe void EnsureWindowInfo()
        {
            if (info.cbSize == 0)
            {
                info.cbSize = sizeof (WINDOWINFO);
                if ( !GetWindowInfo(handle, out info) ) 
                    throw new ApplicationException("Unable to get Window Info");

            }
        }

        public static Window GetForeground()
        {
            IntPtr handle = GetForegroundWindow();
            if (handle == IntPtr.Zero)
                return null;

            return new Window(handle);
        }

        public unsafe static Window GetFocus()
        {
            IntPtr foreground = GetForegroundWindow();
            int procId;
            int tId = GetWindowThreadProcessId(foreground, out procId);
            if (0 != tId)
            {
                GUITHREADINFO threadInfo = new GUITHREADINFO() {cbSize = sizeof (GUITHREADINFO)};
                if ( GetGUIThreadInfo(tId, out threadInfo) )
                {
                    return new Window(threadInfo.hwndFocus);
                }
            }
            return null;
        }

        [StructLayout(LayoutKind.Sequential)]
        private struct WINDOWINFO
        {
            public int cbSize;
            public RECT rcWindow;
            public RECT rcClient;
            public int dwStyle;
            public int dwExStyle;
            public int dwWindowStatus;
            public uint cxWindowBorders;
            public uint cyWindowBorders;
            public int atomWindowType;
            public int wCreatorVersion;
        } 

        [StructLayout(LayoutKind.Sequential)]
        private struct GUITHREADINFO
        {

            public int cbSize;
            public int flags;
            public IntPtr hwndActive;
            public IntPtr hwndFocus;
            public IntPtr hwndCapture;
            public IntPtr hwndMenuOwner;
            public IntPtr hwndMoveSize;
            public IntPtr hwndCaret;
            public RECT rcCaret;
        }

        [StructLayout(LayoutKind.Sequential)]
        private struct RECT
        {
            public int left;
            public int top;
            public int right;
            public int bottom;

            public static implicit operator Rectangle(RECT rhs)
            {
                return new Rectangle(rhs.left, rhs.top, rhs.right - rhs.left, rhs.bottom - rhs.top);
            }
        }

        [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
        private static extern bool GetWindowInfo(IntPtr hwnd, out WINDOWINFO pwi);

        [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
        private static extern int GetWindowThreadProcessId(IntPtr handle, out int processId);

        [DllImport("user32.dll", CharSet = CharSet.Auto, ExactSpelling = true)]
        private static extern bool GetGUIThreadInfo(int threadId, out GUITHREADINFO threadInfo);

        [DllImport("user32.dll", CharSet = CharSet.Auto, ExactSpelling = true)]
        private static extern IntPtr GetForegroundWindow();

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

        [DllImport("user32", CharSet = CharSet.Auto, SetLastError = true)]
        private static extern int GetWindowText(IntPtr hWnd, [Out, MarshalAs(UnmanagedType.LPTStr)] StringBuilder lpString, int nMaxCount);
    }
}

Then we can make a sample program using it:

using System;
using System.Drawing;
using System.Drawing.Imaging;
using System.Threading;

namespace dr.Stackoverflow.ScreenshotTest
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("Sleeping for 3 seconds (switch to a window of interest)");
            Thread.Sleep(3000);

            Window currentWindow = Window.GetForeground();
            Window focusedWindow = Window.GetFocus();       
            if ( currentWindow != null )
            {
                Console.WriteLine("Foreground window");
                Console.WriteLine(currentWindow.Text);
                Console.WriteLine(currentWindow.WindowArea);
            }
            if (focusedWindow != null)
            {
                Console.WriteLine("\tFocused window");
                Console.WriteLine("\t{0}", focusedWindow.WindowArea);
            }

            if (focusedWindow !=null && currentWindow != null && focusedWindow.Handle != currentWindow.Handle)
            {
                Console.WriteLine("\nTaking a screenshot");
                Rectangle screenshotArea = currentWindow.WindowArea;
                Bitmap bm = new Bitmap(currentWindow.WindowArea.Width,currentWindow.WindowArea.Height);
                using(Graphics g = Graphics.FromImage(bm))
                {
                    g.CopyFromScreen(screenshotArea.Left,screenshotArea.Top, 0,0, new Size(screenshotArea.Width,screenshotArea.Height));
                    Rectangle focusBox = focusedWindow.WindowArea;
                    focusBox.Offset(screenshotArea.Left * -1, screenshotArea.Top * -1);
                    focusBox.Inflate(5,5);
                    g.DrawRectangle(Pens.Red,focusBox);
                }
                bm.Save("D:\\screenshot.png", ImageFormat.Png);
            }
        }
    }
}

This will make a screenshot of the current foreground window with a red box highlighting the currently focused control. Please be aware that this is sample code and features minimal error-checking :-) When you run it, Alt-Tab to the window of interest and stay there until the program finishes.

There are some limitations though. The most important one i discovered is that this approach won't work in a WPF application - simply because the individual controls is not Windows, as they are in other Windows programs.