0

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.

David Heffernan
  • 601,492
  • 42
  • 1,072
  • 1,490
  • 2
    Windows doesn't work like that, and doesn't offer a means to do what you are trying to do. Your code that you think works, doesn't. As soon as the desktop is refreshed your drawing will be removed. What you probably want is to create a layered window and place it over the target window. You can then draw in that layered window to achieve the desired effect. – David Heffernan Sep 27 '20 at 08:04
  • Quote : _Since, under Windows, windows are required to draw on demand, the only way to do this is to use SetWindowsHookEx() to hook the WM_PAINT window for that window. This is a non-trivial process that requires that your paint code reside in a DLL (so the DLL can be loaded into the target app's address space). In general, this is not a very good idea unless you have a very good reason to do this._ – TaW Sep 27 '20 at 08:14
  • 2
    *"I found the following code to draw on the desktop that works perfectly fine."* - Sure, that works perfectly fine. As long as you ignore when it fails to produce the desired effect. Which can happen at any time. You neither control when that happens nor can you observe that it did. – IInspectable Sep 27 '20 at 08:54

1 Answers1

1

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.

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 notepad for example. This is what you should see:

enter image description here

Simon Mourier
  • 132,049
  • 21
  • 248
  • 298
  • Thank you that helped me a lot. But how do I add this overlay to a specific window? Sorry if this is a really simple process but I am very new to this and couldn't find anything about that. – Konstantin Kilbel Sep 29 '20 at 13:08
  • Well, in this sample, the overlay covers the whole screen and remembers each window, so you don't need to add an overlay per window. You could create an overlay per window, but I'm not sure it would be useful. In this case, just create one overlay per window and make sure its position matches the window it covers. – Simon Mourier Sep 29 '20 at 13:16
  • Ok yeah that makes sense but I need the app to draw over one specific Window and only over this and the point I don't know how to do it is adding the specific window to the overlay. Because my app requires the app I want to draw over started otherwise it will crash. So I need something to add the tracking for example to the process without a WindowOpenedEvent. Can you help me with that pls? – Konstantin Kilbel Sep 29 '20 at 15:30
  • The sample just adds a window using it's bounding rect (left, top, right bottom). Just get your window's bounding rect by any way you like and adapt the sample. Or ask another question. – Simon Mourier Sep 29 '20 at 15:41