Search code examples
vb.netresizeexplorer

How can I open a File Explorer window from a Visual Studio app and set the position and size?


I have a VB.net application that compacts JPG files, renames them, and copies them from one location to another. When the end-user uses the program, they'll open two file explorer windows to get the source and destination locations and drag them onto the text boxes.

I have added code that opens two file explorers to set locations, but I would like one windows to be in the lower left size of the screen and the other to be in the lower right. Each would be sized to take up 1/4 of the screen.

Most of what I have found is very old. I've found people who said it can't be done and others who provide very old code that doesn't seem to play nice with Visual Studio 2019.

Private Sub btnOpenExplorer_Click(sender As Object, e As EventArgs) Handles btnOpenExplorer.Click

    Process.Start("explorer.exe", String.Format("/n, /e, {0}", "C:\Users\" & Environment.UserName & "\Box\Site Visit Photos"))
    Process.Start("explorer.exe", String.Format("/n, /e, {0}", "P:\"))

End Sub

The above code works well. I just need to add sizing and positioning.


Solution

  • Solution 1

    The Problem

    I think it's difficult to do it in the caller routine btnOpenExplorer_Click since it will be too early to get a process object with all of its properties assigned. Mostly, the ProcessMainWindowTitle and the Process.MainWindowHandle properties which are needed to solve this problem. A workaround to do this is to make the caller starts the processes and a Timer to do the positioning and resizing by the SetWindowPos function.

    Here's how I'd do it:

    The API Functions

    <DllImport("user32.dll", EntryPoint:="SetWindowPos")>
    Private Shared Function SetWindowPos(ByVal hWnd As IntPtr, ByVal hWndInsertAfter As IntPtr, ByVal X As Integer, ByVal Y As Integer, ByVal cx As Integer, ByVal cy As Integer, ByVal uFlags As UInteger) As <MarshalAs(UnmanagedType.Bool)> Boolean
    End Function
    
    <DllImport("user32.dll")>
    Private Shared Function IsIconic(hWnd As IntPtr) As <MarshalAs(UnmanagedType.Bool)> Boolean
    End Function
    
    <DllImport("user32.dll")>
    Public Shared Function ShowWindow(hWnd As IntPtr, <MarshalAs(UnmanagedType.I4)> nCmdShow As Integer) As <MarshalAs(UnmanagedType.Bool)> Boolean
    End Function
    

    Class Level Constants & Variables

    Private Const HWND_TOP As Integer = &H0
    Private Const SW_SHOWNORMAL As Integer = &H1
    
    Private dir1, dir2 As String
    Private WithEvents Timer1 As New Timer With {.Interval = 250}
    

    Process Finder

    Private Function GetExplorerProcess(title As String) As Process
        Dim dirName As String = If(IO.Directory.Exists(title), New IO.DirectoryInfo(title).Name, title).ToLower
    
        Return Process.GetProcesses.Where(
            Function(a) a.ProcessName.ToLower.Equals("explorer") AndAlso
            a.MainWindowTitle.ToLower.Equals(dirName)
            ).FirstOrDefault
    End Function
    

    The Caller

    Private Sub btnOpenExplorer_Click(sender As Object, e As EventArgs) Handles btnOpenExplorer.Click
        Dim proc1 As Process = GetExplorerProcess(dir1)
    
        If proc1 Is Nothing Then
            Dim procInfo1 As New ProcessStartInfo With {
            .FileName = "explorer.exe",
            .Arguments = dir1,
            .WindowStyle = ProcessWindowStyle.Normal
            }
    
            Process.Start(procInfo1)
        End If
    
        Dim proc2 As Process = GetExplorerProcess(dir2)
    
        If proc2 Is Nothing Then
            Dim procInfo2 As New ProcessStartInfo With {
            .FileName = "explorer.exe",
            .Arguments = dir2,
            .WindowStyle = ProcessWindowStyle.Normal
            }
    
            Process.Start(procInfo2)
        End If
    
        Timer1.Start()
    End Sub
    

    The Timer - Activate the Windows, Set Both Size & Location

    Private Sub Timer1_Tick(sender As Object, e As EventArgs) Handles Timer1.Tick
        Dim proc1 As Process = GetExplorerProcess(dir1)
        Dim proc2 As Process = GetExplorerProcess(dir2)
    
        If proc1 IsNot Nothing AndAlso proc2 IsNot Nothing Then
            Timer1.Stop()
    
            Dim R As Rectangle = Screen.PrimaryScreen.WorkingArea
            Dim R1 As New Rectangle(R.X, R.Height - (R.Height / 3), R.Width / 2, R.Height / 4)
            Dim R2 As New Rectangle(R1.Right, R1.Y, R1.Width, R1.Height)
            Dim hWnd1 As IntPtr = proc1.MainWindowHandle
            Dim hWnd2 As IntPtr = proc2.MainWindowHandle
    
            'Restore the first window if its minimized.
            If IsIconic(hWnd1) Then
                ShowWindow(hWnd1, SW_SHOWNORMAL)
            End If
    
            'Set the size and location of the first window.
            SetWindowPos(hWnd1, IntPtr.op_Explicit(HWND_TOP), R1.X, R1.Y, R1.Width, R1.Height, 0)
    
            'Restore the second window if its minimized.
            If IsIconic(hWnd2) Then
                ShowWindow(hWnd2, SW_SHOWNORMAL)
            End If
    
            'Set the size and location of the second window.
            SetWindowPos(hWnd2, IntPtr.op_Explicit(HWND_TOP), R2.X, R2.Y, R2.Width, R2.Height, 0)
        End If
    End Sub
    

    Solution 2

    After some googling here and there, I figured out a better approach (I think) through using the Microsoft Internet Controls - SHDocVw component.

    First, we need to add a reference to that COM component:

    • In your project browser, right-click on the References node and select Add Reference.
    • In the Reference Manager Select the COM tab.
    • Find and check the Microsoft Internet Controls and hit OK.

    We need from solution1 the APIs and the constants only, and a new function to get the IE window:

    Private Function GetIE(dir As String) As SHDocVw.InternetExplorer
        Return (From ie In New SHDocVw.ShellWindows
                Where New Uri(DirectCast(ie, SHDocVw.InternetExplorer).LocationURL).LocalPath.Equals(dir, StringComparison.OrdinalIgnoreCase)
                Select DirectCast(ie, SHDocVw.InternetExplorer)).FirstOrDefault
    End Function
    

    And finally, the caller:

    Private Sub btnOpenExplorer_Click(sender As Object, e As EventArgs) Handles btnOpenExplorer.Click
        Dim dir1 As String = "FirstPath"
        Dim dir2 As String = "SecondPath"
        Dim ie1, ie2 As SHDocVw.InternetExplorer
    
        If Not IO.Path.GetPathRoot(dir1).Equals(dir1, StringComparison.OrdinalIgnoreCase) Then
            dir1 = dir1.TrimEnd(IO.Path.DirectorySeparatorChar)
        End If
    
        If Not IO.Path.GetPathRoot(dir2).Equals(dir2, StringComparison.OrdinalIgnoreCase) Then
            dir2 = dir2.TrimEnd(IO.Path.DirectorySeparatorChar)
        End If
    
        ie1 = GetIE(dir1)
        ie2 = GetIE(dir2)
    
        If ie1 Is Nothing OrElse ie2 Is Nothing Then
            Process.Start(dir1)
            Process.Start(dir2)
            Threading.Thread.Sleep(1000)
        End If
    
        If ie1 Is Nothing Then ie1 = GetIE(dir1)
        If ie2 Is Nothing Then ie2 = GetIE(dir2)
    
        If ie1 IsNot Nothing AndAlso ie2 IsNot Nothing Then
            Dim hWnd1 = IntPtr.op_Explicit(ie1.HWND)
            Dim hWnd2 = IntPtr.op_Explicit(ie2.HWND)
            Dim R As Rectangle = Screen.PrimaryScreen.WorkingArea
            Dim R1 As New Rectangle(R.X, R.Height - (R.Height \ 3), R.Width \ 2, R.Height \ 4)
            Dim R2 As New Rectangle(R1.Right, R1.Y, R1.Width, R1.Height)
    
            SetWindowPos(hWnd1, IntPtr.op_Explicit(HWND_TOP), R2.X, R2.Y, R2.Width, R2.Height, 0)
            SetWindowPos(hWnd2, IntPtr.op_Explicit(HWND_TOP), R1.X, R1.Y, R1.Width, R1.Height, 0)
    
            If IsIconic(hWnd1) Then
                ShowWindow(hWnd1, SW_SHOWNORMAL)
            End If
    
            If IsIconic(hWnd2) Then
                ShowWindow(hWnd2, SW_SHOWNORMAL)
            End If
        End If
    End Sub
    

    Please note, this solution works also with drives (IE: c:\, d:\ ..etc.) while the first one doesn't.

    Here's a demo:

    IEDemo