28
27

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

PowerShellでExplorerウィンドウを自動配置するスクリプト

Posted at

はじめに

image.png

開発作業をしていると、複数のフォルダを同時に開いて作業することがよくあります。しかし、毎回手動でExplorerウィンドウのサイズや位置を調整するのは面倒ですよね。

そこで今回は、PowerShellを使ってExplorerウィンドウを画面上に自動配置するスクリプトを作成しました。指定したフォルダを開いて、画面を綺麗に分割して表示してくれる便利なツールです。

配列で指定したフォルダを自動で開き、画面分割で並べるスクリプトです。ファイルの比較や確認に便利です。

image.png

機能概要

このスクリプトは、あらかじめ指定した複数のフォルダを自動で開き、画面サイズに合わせてウィンドウを整然と配置します。配置は「横一列」または「自動グリッド」から選べ、画面を分割して最適なレイアウトに並べ替えます。プライマリディスプレイの解像度を自動取得してサイズを調整するため、手作業のドラッグ調整が不要になります。

実装のポイント

1. Win32 APIの活用

ウィンドウの位置・サイズ変更にはMoveWindow APIを使用しています:

Add-Type @"
using System;
using System.Runtime.InteropServices;
public class WinAPI {
    [DllImport("user32.dll", SetLastError=true)]
    public static extern bool MoveWindow(IntPtr hWnd, int X, int Y, int nWidth, int nHeight, bool bRepaint);
}
"@

2. Shell.Applicationによるウィンドウ制御

ExplorerウィンドウはCOMオブジェクトShell.Applicationを通して制御します:

$script:shell = New-Object -ComObject Shell.Application

この方法により、ウィンドウハンドル(HWND)を取得して、特定のフォルダを表示しているExplorerを識別できます。

3. 新規ウィンドウの確実な検出

既存のExplorerウィンドウと新規作成されたウィンドウを区別するため、以下の手順を実装しています:

  1. 開く前のウィンドウ一覧を記録
  2. explorer.exe /n で新規ウィンドウを強制作成
  3. タイムアウト付きで目標パスのウィンドウを検索
function Open-ExplorerAndGetWindow {
    param(
        [Parameter(Mandatory)][string]$Path,
        [int]$TimeoutSec = 10
    )
    # 既存HWNDのスナップショット
    $before = @($script:shell.Windows() | ForEach-Object { $_.HWND })
    
    # 新規ウィンドウをなるべく強制
    Start-Process explorer.exe -ArgumentList "/n,`"$Path`""
    
    # 新規ウィンドウを検出...
}

4. パス正規化の重要性

Explorerから取得するパス(LocationURL)とファイルシステムパスを正確に比較するため、パスの正規化処理を実装:

function Get-LocalPathFromLocationURL {
    param([string]$LocationURL)
    try {
        if (-not $LocationURL) { return $null }
        $u = [Uri]$LocationURL
        if ($u.Scheme -ne 'file') { return $null }
        return $u.LocalPath.TrimEnd('\')
    } catch { return $null }
}

レイアウトアルゴリズム

横一列配置(row)

画面を縦に分割して、フォルダを横一列に配置します:

if ($Layout -ieq 'row') {
    $tileW = [int]([math]::Floor($screenW / $N))
    $tileH = $screenH
    for ($i = 0; $i -lt $N; $i++) {
        $x = $tileW * $i
        $w = $tileW
        if ($i -eq ($N - 1)) { $w = $screenW - $x }  # 端の余りを吸収
        # ウィンドウ配置...
    }
}

グリッド配置(grid)

フォルダ数に応じて、ほぼ正方形に近いグリッドを自動計算します:

$cols = [int][math]::Ceiling([math]::Sqrt($N))
$rows = [int][math]::Ceiling($N / $cols)

この計算により、例えば:

  • 3個のフォルダ → 2×2グリッド
  • 6個のフォルダ → 3×2グリッド
  • 8個のフォルダ → 3×3グリッド

のように最適な配置が決まります。

設定とカスタマイズ

スクリプトの上部で簡単に設定を変更できます:

# 並べたいフォルダ(順に配置)
$folders = @(
    "C:\Project\Src",
    "C:\Project\Docs"
    # "C:\Project\Log"  # コメントアウトで除外
)

# レイアウト: 'row' = 横一列, 'grid' = 自動グリッド
$Layout = 'grid'

使用シーン

image.png

このスクリプトは、複数のフォルダを自動で開いて画面に整然と並べることで、日常の作業を一気に効率化します。開発ではソースコード・ドキュメント・テストデータを同時表示でき、モジュールごとの並行作業もはかどります。ファイル整理ではフォルダ間の移動・コピーや、バックアップ前後の比較がスムーズに行えます。プレゼン時にはデモ用の複数フォルダを同時に表示でき、説明がより伝わりやすくなります。

まとめ

このスクリプトにより、手動での面倒なウィンドウ配置作業から解放され、開発効率が大幅に向上します。PowerShellの柔軟性とWin32 APIの組み合わせで、Windowsの標準機能を超えたカスタマイズが可能になります。

設定ファイル化やホットキー対応など、さらなる機能拡張も考えられるので、ぜひ皆さんの環境に合わせてカスタマイズしてみてください!

実装例

Windows 11 上の Windows PowerShell 5.1 で動作確認しています。2~3個のフォルダを並べるのは確認しました。

# ===== 設定 =====
# 並べたいフォルダ(左→右の配置順):ここに開きたいパスを列挙してください
$folders = @(
    "C:\Project\Src",
    "C:\Project\Docs"
    "C:\Project\Log"
)
# レイアウト: 'row' = 横一列, 'grid' = 自動グリッド
$Layout = 'grid'

# ===== Win32 API: MoveWindow =====
Add-Type @"
using System;
using System.Runtime.InteropServices;
public class WinAPI {
    [DllImport("user32.dll", SetLastError=true)]
    public static extern bool MoveWindow(IntPtr hWnd, int X, int Y, int nWidth, int nHeight, bool bRepaint);
}
"@

# 画面サイズ(プライマリ)
Add-Type -AssemblyName System.Windows.Forms | Out-Null
$bounds  = [System.Windows.Forms.Screen]::PrimaryScreen.Bounds
$screenW = $bounds.Width
$screenH = $bounds.Height

# Shell.Application (Explorer制御)
$script:shell = New-Object -ComObject Shell.Application

function Get-LocalPathFromLocationURL {
    param([string]$LocationURL)
    try {
        if (-not $LocationURL) { return $null }
        $u = [Uri]$LocationURL
        if ($u.Scheme -ne 'file') { return $null }
        return $u.LocalPath.TrimEnd('\')
    } catch { return $null }
}

function Open-ExplorerAndGetWindow {
    param(
        [Parameter(Mandatory)][string]$Path,
        [int]$TimeoutSec = 10
    )
    # 既存HWNDのスナップショット
    $before = @($script:shell.Windows() | ForEach-Object { $_.HWND })

    # 新規ウィンドウをなるべく強制
    Start-Process explorer.exe -ArgumentList "/n,`"$Path`""
    Start-Sleep -Milliseconds 300

    # 目標パスを正規化
    if (Test-Path -LiteralPath $Path) {
        $target = (Resolve-Path -LiteralPath $Path).Path.TrimEnd('\')
    } else {
        $target = $Path.TrimEnd('\')
    }

    $deadline = (Get-Date).AddSeconds($TimeoutSec)

    while ((Get-Date) -lt $deadline) {
        $wins  = @($script:shell.Windows() | ForEach-Object { $_ })
        if ($wins.Count -eq 0) { Start-Sleep -Milliseconds 200; continue }

        $added = $wins | Where-Object { $before -notcontains $_.HWND }

        foreach ($w in $added) {
            $local = Get-LocalPathFromLocationURL $w.LocationURL
            if ($local -and ($local -ieq $target)) { return $w }
        }
        foreach ($w in $wins) {
            $local = Get-LocalPathFromLocationURL $w.LocationURL
            if ($local -and ($local -ieq $target)) { return $w }
        }

        Start-Sleep -Milliseconds 250
    }

    # タイムアウト時は最後のExplorerを返す(最終手段)
    return (@($script:shell.Windows() | ForEach-Object { $_ }) | Select-Object -Last 1)
}

function Place-Window {
    param(
        [Parameter(Mandatory)]$Win,
        [int]$X, [int]$Y, [int]$W, [int]$H
    )
    if ($null -ne $Win -and $Win.HWND) {
        [WinAPI]::MoveWindow([IntPtr]$Win.HWND, [int]$X, [int]$Y, [int]$W, [int]$H, $true) | Out-Null
    }
}

# ===== レイアウト計算 =====
$N = [Math]::Max(1, $folders.Count)

if ($Layout -ieq 'row') {
    # 横一列(N分割)
    $tileW = [int]([math]::Floor($screenW / $N))
    $tileH = $screenH
    for ($i = 0; $i -lt $N; $i++) {
        $win  = Open-ExplorerAndGetWindow -Path $folders[$i]
        $x    = $tileW * $i
        $w    = $tileW
        if ($i -eq ($N - 1)) { $w = $screenW - $x }  # 端の余りを吸収
        Place-Window -Win $win -X $x -Y 0 -W $w -H $tileH
    }
}
else {
    # 自動グリッド(ほぼ正方に近い行列)
    $cols  = [int][math]::Ceiling([math]::Sqrt($N))
    $rows  = [int][math]::Ceiling($N / $cols)
    $tileW = [int]([math]::Floor($screenW / $cols))
    $tileH = [int]([math]::Floor($screenH / $rows))

    for ($i = 0; $i -lt $N; $i++) {
        $win  = Open-ExplorerAndGetWindow -Path $folders[$i]
        $r    = [int]([math]::Floor($i / $cols))
        $c    = $i % $cols
        $x    = $tileW * $c
        $y    = $tileH * $r

        $w = $tileW
        if ($c -eq ($cols - 1)) { $w = $screenW - $x }  # 最終列は余りを含める
        $h = $tileH
        if ($r -eq ($rows - 1)) { $h = $screenH - $y }  # 最終行は余りを含める

        Place-Window -Win $win -X $x -Y $y -W $w -H $h
    }
}

参考資料

28
27
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
28
27

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?