Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Move window when external application's window moves

I've got an always on-top application (basically a status display) that I want to follow around another program and always sit just to the left of the minimize button.

I can get the Rect representing the "target" process using the following code which I can then pair with an offset to generate the initial position of my overlay.

Get the HWnd IntPtr:

private IntPtr HWnd = Process.GetProcessesByName("targetapplication")[0].MainWindowHandle; 

Declare the function from user32.dll:

[DllImport("user32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool GetWindowRect(IntPtr hWnd, ref RECT lpRect);

And the appropriate struct:

[StructLayout(LayoutKind.Sequential)]
private struct RECT
{
    public int Left;
    public int Top;
    public int Right;
    public int Bottom;
}

And then call it on demand.

However, I would like to avoid constantly polling this value, so I would like to hook into the target application and respond whenever the target window is moved.

Looking around the user32.dll documentation, the only way I can see for doing this is using SetWindowsHookEx(). I'm not entirely sure of how I'd go about intercepting an event from here however.

I believe the target application is built off of WinForms but I cannot be sure. So solutions that let me respond to the target's Move event or directly to some Windows Message would both be useful.

Any ideas on how I can proceed?

like image 830
Persistence Avatar asked Feb 13 '18 12:02

Persistence


People also ask

How do you move a window when the top is off the screen?

Hold down the Shift key, then right-click on the appropriate application icon in the Windows taskbar. On the resulting pop-up, select the Move option. Begin pressing the arrow keys on your keyboard to move the invisible window from off-screen to on-screen.

How do I move a window that is off screen Windows 11?

Right-click the program on the taskbar, and then click Move. Move the mouse pointer to the middle of the screen. Use the ARROW keys on the keyboard to move the program window to a viewable area on the screen. Press ENTER.


2 Answers

A method to hook a Windows Form to another process (Notepad, in this case) and follow the movements of the process' Main Window, to create sort of a Toolbar that can interact with the process, using SetWinEventHook().


To get the hooked Window bounds, GetWindowRect() is not recommended. Better call DwmGetWindowAttribute() passing DWMWA_EXTENDED_FRAME_BOUNDS as the DWMWINDOWATTRIBUTE, which still returns a RECT. This because GetWindowRect() is not DpiAware and it may return a virtualized measure in some contexts, plus it includes the Window drop shadow in specific configurations.

Read more about DpiAwarenes, Screen layout and VirtualScreen here:
Using SetWindowPos with multiple monitors

Also refactored the Hook helper class and the NativeMethods declarations.


▶ To move to foreground the Tool Window, as in the animation, when the hooked Window becomes the Foreground Window, also hook the EVENT_SYSTEM_FOREGROUND event: you'll receive a notification when the Foreground Window changes, then compare with the handle of Window you're hooking.


A visual representation of the results:

SetWinEventHook sample image

The Form class initialization procedure:

using System.Diagnostics;
using System.Drawing;
using System.Linq;
using System.Runtime.InteropServices;
using System.Windows.Forms;

public partial class Form1 : Form
{
    private IntPtr notepadhWnd;
    private IntPtr hWinEventHook;
    private Process targetProc = null;

    protected Hook.WinEventDelegate WinEventDelegate;
    static GCHandle GCSafetyHandle;

    public Form1()
    {
        InitializeComponent();
        WinEventDelegate = new Hook.WinEventDelegate(WinEventCallback);
        GCSafetyHandle = GCHandle.Alloc(WinEventDelegate);

        targetProc = Process.GetProcessesByName("notepad").FirstOrDefault(p => p != null);

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

                if (notepadhWnd != IntPtr.Zero) {
                    uint targetThreadId = Hook.GetWindowThread(notepadhWnd);

                    hWinEventHook = Hook.WinEventHookOne(
                        NativeMethods.SWEH_Events.EVENT_OBJECT_LOCATIONCHANGE, 
                        WinEventDelegate, (uint)targetProc.Id, targetThreadId);

                    var rect = Hook.GetWindowRectangle(notepadhWnd);
                    // Of course, set the Form.StartPosition to Manual
                    Location = new Point(rect.Right, rect.Top);
                }
            }
        }
        catch (Exception ex) {
            // Log and message or
            throw;
        }
    }

    protected void WinEventCallback(
        IntPtr hWinEventHook, 
        NativeMethods.SWEH_Events eventType, 
        IntPtr hWnd, 
        NativeMethods.SWEH_ObjectId idObject, 
        long idChild, uint dwEventThread, uint dwmsEventTime)
    {
        if (hWnd == notepadhWnd && 
            eventType == NativeMethods.SWEH_Events.EVENT_OBJECT_LOCATIONCHANGE && 
            idObject == (NativeMethods.SWEH_ObjectId)NativeMethods.SWEH_CHILDID_SELF) 
        {
            var rect = Hook.GetWindowRectangle(hWnd);
            Location = new Point(rect.Right, rect.Top);
        }
    }

    protected override void OnFormClosing(FormClosingEventArgs e)
    {
        base.OnFormClosing(e);
        if (!e.Cancel) {
            if (GCSafetyHandle.IsAllocated) GCSafetyHandle.Free();
            Hook.WinEventUnhook(hWinEventHook);
        }
    }

    protected override void OnShown(EventArgs e)
    {
        if (targetProc == null) {
            this.Hide();
            MessageBox.Show("Notepad not found!", "Target Missing", MessageBoxButtons.OK, MessageBoxIcon.Hand);
            this.Close();
        }
        else {
            Size = new Size(50, 140);
            targetProc.Dispose();
        }
        base.OnShown(e);
    }

The support classes used to reference the Windows API methods:

using System.Runtime.InteropServices;
using System.Security.Permissions;

public class Hook
{
    public delegate void WinEventDelegate(
        IntPtr hWinEventHook,
        NativeMethods.SWEH_Events eventType,
        IntPtr hwnd,
        NativeMethods.SWEH_ObjectId idObject,
        long idChild,
        uint dwEventThread,
        uint dwmsEventTime
    );

    public static IntPtr WinEventHookRange(
        NativeMethods.SWEH_Events eventFrom, NativeMethods.SWEH_Events eventTo,
        WinEventDelegate eventDelegate,
        uint idProcess, uint idThread)
    {
        return NativeMethods.SetWinEventHook(
            eventFrom, eventTo,
            IntPtr.Zero, eventDelegate,
            idProcess, idThread,
            NativeMethods.WinEventHookInternalFlags);
    }

    public static IntPtr WinEventHookOne(
        NativeMethods.SWEH_Events eventId,
        WinEventDelegate eventDelegate,
        uint idProcess,
        uint idThread)
    {
        return NativeMethods.SetWinEventHook(
            eventId, eventId,
            IntPtr.Zero, eventDelegate,
            idProcess, idThread,
            NativeMethods.WinEventHookInternalFlags);
    }

    public static bool WinEventUnhook(IntPtr hWinEventHook) => 
        NativeMethods.UnhookWinEvent(hWinEventHook);

    public static uint GetWindowThread(IntPtr hWnd)
    {
        return NativeMethods.GetWindowThreadProcessId(hWnd, IntPtr.Zero);
    }

    public static NativeMethods.RECT GetWindowRectangle(IntPtr hWnd)
    {
        NativeMethods.DwmGetWindowAttribute(hWnd, 
            NativeMethods.DWMWINDOWATTRIBUTE.DWMWA_EXTENDED_FRAME_BOUNDS, 
            out NativeMethods.RECT rect, Marshal.SizeOf<NativeMethods.RECT>());
        return rect;
    }
}

public static class NativeMethods
{
    [StructLayout(LayoutKind.Sequential)]
    public struct RECT
    {
        public int Left;
        public int Top;
        public int Right;
        public int Bottom;

        public Rectangle ToRectangle() => Rectangle.FromLTRB(Left, Top, Right, Bottom);
    }

    public static long SWEH_CHILDID_SELF = 0;

    //SetWinEventHook() flags
    public enum SWEH_dwFlags : uint
    {
        WINEVENT_OUTOFCONTEXT = 0x0000,     // Events are ASYNC
        WINEVENT_SKIPOWNTHREAD = 0x0001,    // Don't call back for events on installer's thread
        WINEVENT_SKIPOWNPROCESS = 0x0002,   // Don't call back for events on installer's process
        WINEVENT_INCONTEXT = 0x0004         // Events are SYNC, this causes your dll to be injected into every process
    }

    //SetWinEventHook() events
    public enum SWEH_Events : uint
    {
        EVENT_MIN = 0x00000001,
        EVENT_MAX = 0x7FFFFFFF,
        EVENT_SYSTEM_SOUND = 0x0001,
        EVENT_SYSTEM_ALERT = 0x0002,
        EVENT_SYSTEM_FOREGROUND = 0x0003,
        EVENT_SYSTEM_MENUSTART = 0x0004,
        EVENT_SYSTEM_MENUEND = 0x0005,
        EVENT_SYSTEM_MENUPOPUPSTART = 0x0006,
        EVENT_SYSTEM_MENUPOPUPEND = 0x0007,
        EVENT_SYSTEM_CAPTURESTART = 0x0008,
        EVENT_SYSTEM_CAPTUREEND = 0x0009,
        EVENT_SYSTEM_MOVESIZESTART = 0x000A,
        EVENT_SYSTEM_MOVESIZEEND = 0x000B,
        EVENT_SYSTEM_CONTEXTHELPSTART = 0x000C,
        EVENT_SYSTEM_CONTEXTHELPEND = 0x000D,
        EVENT_SYSTEM_DRAGDROPSTART = 0x000E,
        EVENT_SYSTEM_DRAGDROPEND = 0x000F,
        EVENT_SYSTEM_DIALOGSTART = 0x0010,
        EVENT_SYSTEM_DIALOGEND = 0x0011,
        EVENT_SYSTEM_SCROLLINGSTART = 0x0012,
        EVENT_SYSTEM_SCROLLINGEND = 0x0013,
        EVENT_SYSTEM_SWITCHSTART = 0x0014,
        EVENT_SYSTEM_SWITCHEND = 0x0015,
        EVENT_SYSTEM_MINIMIZESTART = 0x0016,
        EVENT_SYSTEM_MINIMIZEEND = 0x0017,
        EVENT_SYSTEM_DESKTOPSWITCH = 0x0020,
        EVENT_SYSTEM_END = 0x00FF,
        EVENT_OEM_DEFINED_START = 0x0101,
        EVENT_OEM_DEFINED_END = 0x01FF,
        EVENT_UIA_EVENTID_START = 0x4E00,
        EVENT_UIA_EVENTID_END = 0x4EFF,
        EVENT_UIA_PROPID_START = 0x7500,
        EVENT_UIA_PROPID_END = 0x75FF,
        EVENT_CONSOLE_CARET = 0x4001,
        EVENT_CONSOLE_UPDATE_REGION = 0x4002,
        EVENT_CONSOLE_UPDATE_SIMPLE = 0x4003,
        EVENT_CONSOLE_UPDATE_SCROLL = 0x4004,
        EVENT_CONSOLE_LAYOUT = 0x4005,
        EVENT_CONSOLE_START_APPLICATION = 0x4006,
        EVENT_CONSOLE_END_APPLICATION = 0x4007,
        EVENT_CONSOLE_END = 0x40FF,
        EVENT_OBJECT_CREATE = 0x8000,               // hwnd ID idChild is created item
        EVENT_OBJECT_DESTROY = 0x8001,              // hwnd ID idChild is destroyed item
        EVENT_OBJECT_SHOW = 0x8002,                 // hwnd ID idChild is shown item
        EVENT_OBJECT_HIDE = 0x8003,                 // hwnd ID idChild is hidden item
        EVENT_OBJECT_REORDER = 0x8004,              // hwnd ID idChild is parent of zordering children
        EVENT_OBJECT_FOCUS = 0x8005,                // hwnd ID idChild is focused item
        EVENT_OBJECT_SELECTION = 0x8006,            // hwnd ID idChild is selected item (if only one), or idChild is OBJID_WINDOW if complex
        EVENT_OBJECT_SELECTIONADD = 0x8007,         // hwnd ID idChild is item added
        EVENT_OBJECT_SELECTIONREMOVE = 0x8008,      // hwnd ID idChild is item removed
        EVENT_OBJECT_SELECTIONWITHIN = 0x8009,      // hwnd ID idChild is parent of changed selected items
        EVENT_OBJECT_STATECHANGE = 0x800A,          // hwnd ID idChild is item w/ state change
        EVENT_OBJECT_LOCATIONCHANGE = 0x800B,       // hwnd ID idChild is moved/sized item
        EVENT_OBJECT_NAMECHANGE = 0x800C,           // hwnd ID idChild is item w/ name change
        EVENT_OBJECT_DESCRIPTIONCHANGE = 0x800D,    // hwnd ID idChild is item w/ desc change
        EVENT_OBJECT_VALUECHANGE = 0x800E,          // hwnd ID idChild is item w/ value change
        EVENT_OBJECT_PARENTCHANGE = 0x800F,         // hwnd ID idChild is item w/ new parent
        EVENT_OBJECT_HELPCHANGE = 0x8010,           // hwnd ID idChild is item w/ help change
        EVENT_OBJECT_DEFACTIONCHANGE = 0x8011,      // hwnd ID idChild is item w/ def action change
        EVENT_OBJECT_ACCELERATORCHANGE = 0x8012,    // hwnd ID idChild is item w/ keybd accel change
        EVENT_OBJECT_INVOKED = 0x8013,              // hwnd ID idChild is item invoked
        EVENT_OBJECT_TEXTSELECTIONCHANGED = 0x8014, // hwnd ID idChild is item w? test selection change
        EVENT_OBJECT_CONTENTSCROLLED = 0x8015,
        EVENT_SYSTEM_ARRANGMENTPREVIEW = 0x8016,
        EVENT_OBJECT_END = 0x80FF,
        EVENT_AIA_START = 0xA000,
        EVENT_AIA_END = 0xAFFF
    }

    //SetWinEventHook() Object Ids
    public enum SWEH_ObjectId : long
    {
        OBJID_WINDOW = 0x00000000,
        OBJID_SYSMENU = 0xFFFFFFFF,
        OBJID_TITLEBAR = 0xFFFFFFFE,
        OBJID_MENU = 0xFFFFFFFD,
        OBJID_CLIENT = 0xFFFFFFFC,
        OBJID_VSCROLL = 0xFFFFFFFB,
        OBJID_HSCROLL = 0xFFFFFFFA,
        OBJID_SIZEGRIP = 0xFFFFFFF9,
        OBJID_CARET = 0xFFFFFFF8,
        OBJID_CURSOR = 0xFFFFFFF7,
        OBJID_ALERT = 0xFFFFFFF6,
        OBJID_SOUND = 0xFFFFFFF5,
        OBJID_QUERYCLASSNAMEIDX = 0xFFFFFFF4,
        OBJID_NATIVEOM = 0xFFFFFFF0
    }

    public enum DWMWINDOWATTRIBUTE : uint
    {
        DWMWA_NCRENDERING_ENABLED = 1,      // [get] Is non-client rendering enabled/disabled
        DWMWA_NCRENDERING_POLICY,           // [set] DWMNCRENDERINGPOLICY - Non-client rendering policy - Enable or disable non-client rendering
        DWMWA_TRANSITIONS_FORCEDISABLED,    // [set] Potentially enable/forcibly disable transitions
        DWMWA_ALLOW_NCPAINT,                // [set] Allow contents rendered In the non-client area To be visible On the DWM-drawn frame.
        DWMWA_CAPTION_BUTTON_BOUNDS,        // [get] Bounds Of the caption button area In window-relative space.
        DWMWA_NONCLIENT_RTL_LAYOUT,         // [set] Is non-client content RTL mirrored
        DWMWA_FORCE_ICONIC_REPRESENTATION,  // [set] Force this window To display iconic thumbnails.
        DWMWA_FLIP3D_POLICY,                // [set] Designates how Flip3D will treat the window.
        DWMWA_EXTENDED_FRAME_BOUNDS,        // [get] Gets the extended frame bounds rectangle In screen space
        DWMWA_HAS_ICONIC_BITMAP,            // [set] Indicates an available bitmap When there Is no better thumbnail representation.
        DWMWA_DISALLOW_PEEK,                // [set] Don't invoke Peek on the window.
        DWMWA_EXCLUDED_FROM_PEEK,           // [set] LivePreview exclusion information
        DWMWA_CLOAK,                        // [set] Cloak Or uncloak the window
        DWMWA_CLOAKED,                      // [get] Gets the cloaked state Of the window. Returns a DWMCLOACKEDREASON object
        DWMWA_FREEZE_REPRESENTATION,        // [set] BOOL, Force this window To freeze the thumbnail without live update
        PlaceHolder1,
        PlaceHolder2,
        PlaceHolder3,
        DWMWA_ACCENTPOLICY = 19
    }

    public static SWEH_dwFlags WinEventHookInternalFlags =
        SWEH_dwFlags.WINEVENT_OUTOFCONTEXT |
        SWEH_dwFlags.WINEVENT_SKIPOWNPROCESS |
        SWEH_dwFlags.WINEVENT_SKIPOWNTHREAD;

    [DllImport("dwmapi.dll", SetLastError = true)]
    internal static extern int DwmGetWindowAttribute(IntPtr hwnd, DWMWINDOWATTRIBUTE dwAttribute, out RECT pvAttribute, int cbAttribute);

    [DllImport("user32.dll", SetLastError = true)]
    internal static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint lpdwProcessId);

    [DllImport("user32.dll")]
    internal static extern uint GetWindowThreadProcessId(IntPtr hWnd, IntPtr voidProcessId);

    [DllImport("user32.dll", SetLastError = false)]
    internal static extern IntPtr SetWinEventHook(
        SWEH_Events eventMin,
        SWEH_Events eventMax,
        IntPtr hmodWinEventProc,
        Hook.WinEventDelegate lpfnWinEventProc,
        uint idProcess, uint idThread,
        SWEH_dwFlags dwFlags);

    [DllImport("user32.dll", SetLastError = false)]
    internal static extern bool UnhookWinEvent(IntPtr hWinEventHook);
}
like image 150
Jimi Avatar answered Oct 23 '22 22:10

Jimi


Thanks to @Jimi for his help here. The following method worked.

First, store a reference to the target process:

Process _target = Process.GetProcessesByName("target")[0];

Then get the handle to the main window:

IntPtr _tagetHWnd = _target.MainWindowHandle;

Then initialise the hook:

SetWinEventHook(EVENT_OBJECT_LOCATIONCHANGE, EVENT_OBJECT_LOCATIONCHANGE, IntPtr.Zero, TargetMoved, (uint)_foxview.Id,
            GetWindowThreadProcessId(_foxview.MainWindowHandle, IntPtr.Zero), WINEVENT_OUTOFCONTEXT | WINEVENT_SKIPOWNPROCESS | WINEVENT_SKIPOWNTHREAD);

Where SetWinEventHook is declared as such:

[DllImport("user32.dll")]
private static extern IntPtr SetWinEventHook(uint eventMin, uint eventMax, IntPtr hmodWinEventProc, WinEventDelegate lpfnWinEventProc, uint idProcess, uint idThread, uint dwFlags);

And the constants involved are:

private const uint EVENT_OBJECT_LOCATIONCHANGE = 0x800B;
private const int HT_CAPTION = 0x2;
private const uint WINEVENT_OUTOFCONTEXT = 0x0000;
private const uint WINEVENT_SKIPOWNPROCESS = 0x0002;
private const uint WINEVENT_SKIPOWNTHREAD = 0x0001;
private const int WM_NCLBUTTONDOWN = 0xA1;

Then in my TargetMoved method I get check the new Window location and move my overlay.

private void TargetMoved(IntPtr hWinEventHook, uint eventType, IntPtr lParam, int idObject, int idChild, uint dwEventThread, uint dwmsEventTime)
{
    Rect newLocation = new Rect();
    GetWindowRect(_foxViewHWnd, ref newLocation);
    Location = new Point(newLocation.Right - (250 + _currentUser.Length * 7), newLocation.Top + 5);
}

Where GetWindowRect() is defined by:

[DllImport("user32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool GetWindowRect(IntPtr hWnd, ref Rect lpRect);

And Rect is defined by:

[StructLayout(LayoutKind.Sequential)]
private struct Rect
{
    public readonly int Left;
    public readonly int Top;
    public readonly int Right;
    public readonly int Bottom;
}

So when you put it all together, the entire class now looks like this:

using System;
using System.Diagnostics;
using System.Drawing;
using System.Runtime.InteropServices;
using System.Windows.Forms;
using UserMonitor;

namespace OnScreenOverlay
{
    public partial class Overlay : Form
    {
        #region Public Fields
    
        public const string UserCache = @"redacted";
    
        #endregion Public Fields
    
        #region Private Fields
    
        private const uint EVENT_OBJECT_LOCATIONCHANGE = 0x800B;
        private const uint WINEVENT_OUTOFCONTEXT = 0x0000;
        private const uint WINEVENT_SKIPOWNPROCESS = 0x0002;
        private const uint WINEVENT_SKIPOWNTHREAD = 0x0001;
        private readonly Process _foxview;
        private readonly IntPtr _foxViewHWnd;
        private readonly UserMon _monitor;
        private string _currentUser;
    
        #endregion Private Fields
    
        #region Public Constructors
    
        public Overlay()
        {
            InitializeComponent();
            _target= Process.GetProcessesByName("target")[0];
            if (_foxview == null)
            {
                MessageBox.Show("No target detected... Closing");
                Close();
            }
            _targetHWnd = _target.MainWindowHandle;
            InitializeWinHook();
            StartPosition = FormStartPosition.Manual;
            Location = new Point(Screen.PrimaryScreen.Bounds.Left + 20, Screen.PrimaryScreen.Bounds.Bottom - 20);
            ShowInTaskbar = false;
            _monitor = new UserMon(UserCache);
            _monitor.UserChanged += (s, a) =>
            {
                _currentUser = a.Value;
                if (pictBox.InvokeRequired)
                {
                    pictBox.Invoke((MethodInvoker)delegate { pictBox.Refresh(); });
                }
            };
            _currentUser = _monitor.GetUser();
        }
    
        #endregion Public Constructors
    
    
    
        #region Private Delegates
    
        private delegate void WinEventDelegate(IntPtr hWinEventHook, uint eventType,
                            IntPtr hwnd, int idObject, int idChild, uint dwEventThread, uint dwmsEventTime);
    
        #endregion Private Delegates
    
    
    
        #region Private Methods
    
        [DllImport("user32.dll", SetLastError = true)]
        [return: MarshalAs(UnmanagedType.Bool)]
        private static extern bool GetWindowRect(IntPtr hWnd, ref Rect lpRect);
    
        [DllImport("user32.dll", SetLastError = true)]
        private static extern uint GetWindowThreadProcessId(IntPtr hWnd, IntPtr processId);
    
        [DllImport("user32.dll")]
        private static extern IntPtr SetWinEventHook(uint eventMin, uint eventMax, IntPtr
                hmodWinEventProc, WinEventDelegate lpfnWinEventProc, uint idProcess,
            uint idThread, uint dwFlags);
    
        private void InitializeWinHook()
        {
            SetWinEventHook(EVENT_OBJECT_LOCATIONCHANGE, EVENT_OBJECT_LOCATIONCHANGE, IntPtr.Zero, TargetMoved, (uint)_foxview.Id,
                GetWindowThreadProcessId(_foxview.MainWindowHandle, IntPtr.Zero), WINEVENT_OUTOFCONTEXT | WINEVENT_SKIPOWNPROCESS | WINEVENT_SKIPOWNTHREAD);
        }
    
        private void Overlay_FormClosing(object sender, FormClosingEventArgs e)
        {
            _monitor.Dispose();
        }
    
        private void pictBox_Paint(object sender, PaintEventArgs e)
        {
            using (Font myFont = new Font("Arial", 8))
            {
                e.Graphics.DrawString($"User: {_currentUser}", myFont, Brushes.LimeGreen, new Point(2, 2));
            }
        }
    
        private void TargetMoved(IntPtr hWinEventHook, uint eventType, IntPtr lParam, int idObject, int idChild, uint dwEventThread, uint dwmsEventTime)
        {
            Rect newLocation = new Rect();
            GetWindowRect(_foxViewHWnd, ref newLocation);
            Location = new Point(newLocation.Right - (250 + _currentUser.Length * 7), newLocation.Top + 5);
        }
    
        #endregion Private Methods
    
    
    
        #region Private Structs
    
        [StructLayout(LayoutKind.Sequential)]
        private struct Rect
        {
            public readonly int Left;
            public readonly int Top;
            public readonly int Right;
            public readonly int Bottom;
        }
    
        #endregion Private Structs
    }
}
like image 43
Persistence Avatar answered Oct 24 '22 00:10

Persistence