Search code examples
c#vb.netwinformswindowcommand-line-interface

How to embed a external CLI window into a Panel in Windows Forms?


In C# or VB.NET, under Windows Forms, I would like to know how can I embed a external command-line interface (CLI) window, into a panel or other kind of host window where I can render the contents of the external CLI window inside my form.

Please note that I don't pretend to redirect and print the StdOut stream by myself. I just would like to embed the window into my form and let it be.

I've tried SetParent function as suggested, but it does not seem to work for non-graphical user-interface windows, because when setting the parent window, the CLI window disappear from screen and it is not rendered in the parent (a panel) window.

It seems this can be done in WPF as suggested here, but I'm not aware how to do this under Windows Forms.

I'm looking for some workaround that I could use in a way like this:

using (var p = new Process()) {
        p.StartInfo.FileName = @".\cli-process.exe";
        p.StartInfo.Arguments = @"...";
        p.StartInfo.CreateNoWindow = false;
        p.StartInfo.UseShellExecute = false;
        p.StartInfo.WindowStyle = ProcessWindowStyle.Normal;
        p.Start();

        Thread.Sleep(2000);
        EmbedToWindow(p.MainWindowHandle, this.Panel1.Handle);
        p.WaitForExit(TimeOut.Infinite);
    }
}

Solution

  • I just wrote a simple helper class in VB.NET that will serve me to set and release a parent window with ease.

    It seems to work as expected at least as far as I have tested it in my required scenarios.

    Thanks to @Jimi, @RbMm and @Remy Lebeau for their help and their tips that I need to know in order to figure how to do this.

    However, what I share here is not functional due to missing types, but you could get the idea. Sorry but it would require to exceed the maximum character limit of this post, and maybe it's too much effort of copy & paste and adapt things to show them here if I'm the only one interested to accomplish this task...

    But if you want to figure how to make this class functional then simply add the missing P/Invoke members of which I use in the missing NativeMethods class definition, and replace the missing WindowInfo type for a call to GetWindowInfo (and the WINDOWINFO struct) or else GetClientRect + GetWindowLongPtr functions, and replace missing SafeWindowHandle type for IntPtr or for a custom class derived from SafeHandleZeroOrMinusOneIsInvalid.

    Usage Example:

    Dim process As Process = Process.GetProcessesByName("name").Single()
    Dim windowParenting As New WindowParenting(process.MainWindowHandle)
    
    windowParenting.SetParent(Me.Panel1, 
                              fitToParentBounds:=True, 
                              removeBorder:=True, 
                              removeCaption:=True, 
                              resizable:=False)
    
    Thread.Sleep(5000)
    windowParenting.ReleaseParent(throwOnInvalidSourceWindowHandle:=True)
    
    ' Or...
    ' windowParenting.Dispose() ' It calls ReleaseParent.
    

    WindowParenting Class:

    ''' ----------------------------------------------------------------------------------------------------
    ''' <summary>
    ''' Implements a mechanism to set and release the parent window for a specific source window.
    ''' </summary>
    ''' ----------------------------------------------------------------------------------------------------
    Public Class WindowParenting : Implements IDisposable
    
    #Region " Properties"
    
        ''' <summary>
        ''' A safe handle to the source window.
        ''' </summary>
        Public ReadOnly Property WindowHandle As SafeWindowHandle
    
        ''' <summary>
        ''' Gets a <see cref="DevCase.Core.IPC.WindowInfo"/> object for the source window.
        ''' </summary>
        Public ReadOnly Property WindowInfo As WindowInfo
            Get
                Return Me.GetWindowInfo()
            End Get
        End Property
    
        ''' <summary>
        ''' Gets a value that determine whether the source window has a parent window.
        ''' </summary>
        Public ReadOnly Property HasParent As Boolean
            Get
                Try
                    Return Me.WindowInfo.ParentWindow IsNot Nothing
                Catch ex As Exception
                    Return False
                End Try
            End Get
        End Property
    
    #End Region
    
    #Region " Private Fields"
    
        ''' <summary>
        ''' Keeps track of the current source window bounds when making a call to <see cref="WindowParenting.SetParent"/> method.
        ''' </summary>
        Private lastBounds As Rectangle = Rectangle.Empty
    
        ''' <summary>
        ''' Keeps track of the current source <see cref="WindowStyles"/> when making a call to <see cref="WindowParenting.SetParent"/> method.
        ''' </summary>
        Private lastWindowStyle As WindowStyles = WindowStyles.None
    
    #End Region
    
    #Region " Constructors "
    
        ''' <summary>
        ''' Initializes a new instance of the <see cref="WindowParenting"/> class.
        ''' </summary>
        ''' <param name="hwnd">
        ''' A handle to the source window.
        ''' </param>
        <DebuggerStepThrough>
        Public Sub New(hwnd As IntPtr)
            Me.New(New SafeWindowHandle(hwnd))
        End Sub
    
        ''' <summary>
        ''' Initializes a new instance of the <see cref="WindowParenting"/> class.
        ''' </summary>
        ''' <param name="hwnd">
        ''' A handle to the source window.
        ''' </param>
        <DebuggerStepThrough>
        Public Sub New(hwnd As SafeHandle)
            Me.WindowHandle = hwnd
    
            If Me.WindowHandle.IsInvalid Then
                Throw New Exception("Invalid window handle.")
            End If
        End Sub
    
        ''' <summary>
        ''' Initializes a new instance of the <see cref="WindowParenting"/> class.
        ''' </summary>
        ''' <param name="window">
        ''' AThe source window.
        ''' </param>
        <DebuggerStepThrough>
        Public Sub New(window As NativeWindow)
            Me.New(New SafeWindowHandle(window.Handle))
        End Sub
    
        ''' <summary>
        ''' Initializes a new instance of the <see cref="WindowParenting"/> class.
        ''' </summary>
        ''' <param name="window">
        ''' The source window.
        ''' </param>
        <DebuggerStepThrough>
        Public Sub New(window As IWin32Window)
            Me.New(New SafeWindowHandle(window.Handle))
        End Sub
    
    #End Region
    
    #Region " Public Methods "
    
        ''' <summary>
        ''' Sets a new parent window for the source window.
        ''' </summary>
        ''' <param name="parentWindow">
        ''' The parent window.
        ''' </param>
        ''' 
        ''' <param name="fitToParentBounds">
        ''' If set to <see langword="True"/>, fits to size of the source window to the parent window bounds.
        ''' </param>
        ''' 
        ''' <param name="removeBorder">
        ''' If set to <see langword="True"/>, removes the border from the source window.
        ''' </param>
        ''' 
        ''' <param name="removeCaption">
        ''' If set to <see langword="True"/>, removes the caption from the source window.
        ''' </param>
        ''' 
        ''' <param name="resizable">
        ''' If set to <see langword="False"/>, remove sthe size frame from the source window.
        ''' </param>
        ''' <exception cref="InvalidOperationException">
        ''' Source window already has a parent window.
        ''' </exception>
        <DebuggerStepThrough>
        Public Overridable Sub SetParent(parentWindow As IWin32Window, fitToParentBounds As Boolean,
                                         removeBorder As Boolean, removeCaption As Boolean,
                                         resizable As Boolean)
    
            Dim curentWindowInfo As WindowInfo = Me.GetWindowInfo()
            If Me.lastBounds = Rectangle.Empty Then
                Me.lastBounds = curentWindowInfo.Bounds
            End If
            If Me.lastWindowStyle = WindowStyles.None Then
                Me.lastWindowStyle = curentWindowInfo.WindowStyle
            End If
    
            Dim newStyle As WindowStyles = (Me.lastWindowStyle And Not WindowStyles.SysMenu)
            If removeBorder Then
                newStyle = (newStyle And Not WindowStyles.Border)
            End If
            If removeCaption Then
                newStyle = (newStyle And Not WindowStyles.Caption)
            End If
            If Not resizable Then
                newStyle = (newStyle And Not WindowStyles.SizeFrame)
            End If
    
            Dim parentWindowHandle As New SafeWindowHandle(parentWindow.Handle)
    
            NativeMethods.SetParent(Me.WindowHandle, parentWindowHandle)
            Me.SetSourceWindowStyle(newStyle)
    
            Dim parentClientRect As Rectangle
            If fitToParentBounds Then
                NativeMethods.GetClientRect(parentWindowHandle, parentClientRect)
            End If
            NativeMethods.SetWindowPos(Me.WindowHandle, IntPtr.Zero, 0, 0,
                                       If(fitToParentBounds, parentClientRect.Width, 0),
                                       If(fitToParentBounds, parentClientRect.Height, 0),
                                       SetWindowPosFlags.AsyncWindowPos Or
                                       SetWindowPosFlags.ShowWindow Or
                                       If(fitToParentBounds, SetWindowPosFlags.None, SetWindowPosFlags.IgnoreResize))
    
        End Sub
    
        Public Overridable Sub SetParent(parentWindow As NativeWindow, fitToParentBounds As Boolean,
                                         removeBorder As Boolean, removeCaption As Boolean,
                                         resizable As Boolean)
    
            Me.SetParent(DirectCast(parentWindow, IWin32Window), fitToParentBounds, removeBorder, removeCaption, resizable)
    
        End Sub
    
        Public Overridable Sub SetParent(parentWindow As Control, fitToParentBounds As Boolean,
                                         removeBorder As Boolean, removeCaption As Boolean,
                                         resizable As Boolean)
    
            Me.SetParent(DirectCast(parentWindow, IWin32Window), fitToParentBounds, removeBorder, removeCaption, resizable)
    
        End Sub
    
        ''' <summary>
        ''' Release the source window from its current parent window.
        ''' </summary>
        ''' <param name="throwOnInvalidSourceWindowHandle">
        ''' If set to <see langword="True"/>, throws an <see cref="NullReferenceException"/> 
        ''' if the source window handle specified in <see cref="WindowParenting.WindowHandle"/> is invalid.
        ''' <para></para>
        ''' This can be useful if you need to detect whether the source window has been destroyed.
        ''' </param>
        <DebuggerStepThrough>
        Public Overridable Sub ReleaseParent(Optional throwOnInvalidSourceWindowHandle As Boolean = False)
    
            Dim isInvalid As Boolean = Me.WindowHandle.IsInvalid OrElse
                                       Me.WindowHandle.IsClosed OrElse
                                       Not NativeMethods.IsWindow(Me.WindowHandle)
    
            If isInvalid AndAlso throwOnInvalidSourceWindowHandle Then
                Throw New NullReferenceException("Invalid source window handle.")
    
            ElseIf Not isInvalid Then
                If Not Me.HasParent Then
                    Throw New InvalidOperationException("Source window has not a parent window.")
                End If
    
                NativeMethods.SetParent(Me.WindowHandle, IntPtr.Zero)
    
                If Me.lastWindowStyle <> WindowStyles.None Then
                    Me.SetSourceWindowStyle(Me.lastWindowStyle)
                    Me.lastWindowStyle = WindowStyles.None
                End If
    
                If Me.lastBounds <> Rectangle.Empty Then
                    NativeMethods.SetWindowPos(Me.WindowHandle, IntPtr.Zero,
                                           Me.lastBounds.X, Me.lastBounds.Y,
                                           Me.lastBounds.Width, Me.lastBounds.Height,
                                           SetWindowPosFlags.AsyncWindowPos)
                    Me.lastBounds = Rectangle.Empty
                End If
    
            End If
    
        End Sub
    
    #End Region
    
    #Region " Private Methods "
    
        ''' <summary>
        ''' Returns a <see cref="DevCase.Core.IPC.WindowInfo"/> object for the source window.
        ''' </summary>
        <DebuggerStepThrough>
        Private Function GetWindowInfo() As WindowInfo
            Return New WindowInfo(Me.WindowHandle)
        End Function
    
        ''' <summary>
        ''' Sets the <see cref="WindowStyles"/> for the source window.
        ''' </summary>
        <DebuggerStepThrough>
        Private Sub SetSourceWindowStyle(style As WindowStyles)
    
            If Environment.Is64BitProcess Then
                NativeMethods.SetWindowLongPtr(Me.WindowHandle, WindowLongValues.WindowStyle, style)
            Else
                NativeMethods.SetWindowLong(Me.WindowHandle, WindowLongValues.WindowStyle, style)
            End If
    
        End Sub
    
    #End Region
    
    #Region " IDisposable Implementation "
    
        ''' <summary>
        ''' Flag to detect redundant calls.
        ''' </summary>
        Private disposedValue As Boolean
    
        ''' <summary>
        ''' Releases all the resources used by this instance.
        ''' </summary>
        ''' <param name="disposing">
        ''' <see langword="True"/> to release both managed and unmanaged resources; 
        ''' <see langword="False"/> to release only unmanaged resources.
        ''' </param>
        Protected Overridable Sub Dispose(disposing As Boolean)
            If Not Me.disposedValue AndAlso disposing Then
                Try
                    Me.ReleaseParent()
                Catch ex As Exception
                End Try
                Me.WindowHandle?.Close()
            End If
            Me.disposedValue = True
        End Sub
    
        ''' <summary>
        ''' Releases all the resources used by this instance.
        ''' </summary>
        Public Sub Dispose() Implements IDisposable.Dispose
            Me.Dispose(True)
        End Sub
    
    #End Region
    
    End Class
    

    It lacks a bit of win32 error-handling for P/invokes. And maybe other things could be improved.