Search code examples
c#winapi

C# drawing on windows


I am trying to draw some lines and rectangles on an existing window. I found the following code to draw on the desktop that works perfectly fine.

class Drawing {
[DLLImport("User32.dll")] 
static extern IntPtr GetDC(IntPtr hWnd);       

    public static void draw(Rectangle r, Brush b, IntPtr hWnd) {
        using(Graphics g = Graphics.FromHdc(hwnd)) {
            g.DrawRectangle(b, r);
        }
    }
}

Drawing.draw(new Rectangle(0, 0, 40, 80), Brushes.Red, Drawing.GetDC(IntPtr.Zero));

This is the code how I modified it to draw on a specific window.

class Drawing {
    public static IntPtr WinGetHandle(string wName) {
        foreach (Process pList in Process.GetProcesses())
            if (pList.MainWindowTitle.Contains(wName))
                return pList.MainWindowHandle;

        return IntPtr.Zero;
    }

    public static void draw(Rectangle r, Brush b, IntPtr hwnd) {
        using(Graphics g = Graphics.FromHwnd(hwnd)) {
            g.DrawRectangle(p, r);
        }
    }
}

Drawing.draw(new Rectangle(0, 0, 40, 80), Brushes.Red, Drawing.WinGetHandle("DrawingWindow"));

With this code nothing happens and in some cases it throws OutOfMemoryException and crashes right after. In the Internet I found some other variants of getting the window handle and then draw on the window but most of the crash with the same exception or just do nothing. I also tried it in a loop because I thought the app may overdraw it every frame.

So my question is has anyone an idea how I could fix this or had the same problem and an other method do this.


Solution

  • As said in the comments, you cannot draw on the screen directly. What you can do is build some "overlay" window (transparent and click-through) and draw on it.

    Here is a C# Console app sample that demonstrates that and also uses UI Automation that track opened windows and draw a yellow rectangle around them.

    // needs a reference to UIAutomationClient, UIAutomationType and WindowsBase
    static class Program
    {
        [STAThread]
        static void Main()
        {
            var overlay = new Overlay();
    
            // track windows open (requires WindowsBase, UIAutomationTypes, UIAutomationClient)
            Automation.AddAutomationEventHandler(WindowPattern.WindowOpenedEvent, AutomationElement.RootElement, TreeScope.Subtree, (s, e) =>
            {
                var element = (AutomationElement)s;
                if (element.Current.ProcessId != Process.GetCurrentProcess().Id)
                {
                    Console.WriteLine("Added window '" + element.Current.Name + "'");
                    overlay.AddTrackedWindow(element);
    
                    // track window close
                    Automation.AddAutomationEventHandler(WindowPattern.WindowClosedEvent, element, TreeScope.Element, (s2, e2) =>
                    {
                        overlay.RemoveTrackedWindow(element);
                    });
                }
            });
    
            Application.Run(overlay);
        }
    }
    
    // adapted from https://stackoverflow.com/questions/11077236/transparent-window-layer-that-is-click-through-and-always-stays-on-top
    public class Overlay : Form // standard Windows Form
    {
        private readonly HashSet<AutomationElement> _windows = new HashSet<AutomationElement>();
    
        public Overlay()
        {
            TopMost = true;
            FormBorderStyle = FormBorderStyle.None;
            WindowState = FormWindowState.Maximized;
            MaximizeBox = false;
            MinimizeBox = false;
            ShowInTaskbar = false;
            BackColor = Color.White;
            TransparencyKey = BackColor;
        }
    
        protected override CreateParams CreateParams
        {
            get
            {
                var cp = base.CreateParams;
                const int WS_EX_TRANSPARENT = 0x20;
                const int WS_EX_LAYERED = 0x80000;
                const int WS_EX_NOACTIVATE = 0x8000000;
                cp.ExStyle |= WS_EX_LAYERED | WS_EX_TRANSPARENT | WS_EX_NOACTIVATE;
                return cp;
            }
        }
    
        protected override void OnPaint(PaintEventArgs e)
        {
            base.OnPaint(e);
    
            foreach (var window in _windows.ToArray())
            {
                Rect rect;
                try
                {
                    rect = window.Current.BoundingRectangle;
                }
                catch
                {
                    // error, window's gone
                    _windows.Remove(window);
                    continue;
                }
    
                // draw a yellow rectangle around window
                using (var pen = new Pen(Color.Yellow, 2))
                {
                    e.Graphics.DrawRectangle(pen, (float)rect.X, (float)rect.Y, (float)rect.Width, (float)rect.Height);
                }
            }
        }
    
        // ensure we call Invalidate on UI thread
        private void InvokeInvalidate() => BeginInvoke((Action)(() => { Invalidate(); }));
    
        public void RemoveTrackedWindow(AutomationElement element)
        {
            _windows.Remove(element);
            InvokeInvalidate();
        }
    
        public void AddTrackedWindow(AutomationElement element)
        {
            _windows.Add(element);
            InvokeInvalidate();
    
            // follow target window position
            Automation.AddAutomationPropertyChangedEventHandler(element, TreeScope.Element, (s, e) =>
            {
                InvokeInvalidate();
            }, AutomationElement.BoundingRectangleProperty);
        }
    }
    

    To test it, run it, and open a normal Win32 desktop app like notepad for example (not the new Windows 11 one) or explorer (this one works fine on Windows 11). This is what you should see:

    enter image description here