跳转至

scripts\ui-shot.ps1

源码路径:scripts\ui-shot.ps1 · 173 行

# 给界面拍图:每页开一次,把窗口挪到左上角置顶,抓窗口矩形存 PNG。
#
# 用法:
#   .\scripts\ui-shot.ps1                          # 默认拍首页 + 插件页
#   .\scripts\ui-shot.ps1 -Pages weather,plugins,topo
#   .\scripts\ui-shot.ps1 -All                     # 功能树上每一页都拍
#   .\scripts\ui-shot.ps1 -OutDir dist\shots
#
# 为什么要置顶再抓:界面是**无边框**窗口,如果被别的窗口压着,
# CopyFromScreen 抓到的就是压在上面的那个窗口 —— 拍出来是一张别人家的截图。
# 所以先 SetWindowPos 挪到 (0,0) 并临时置顶,拍完再撤掉置顶。
#
# 这只是**开发期自检**用的:真正要出文档图,应该在被拍的程序里自己渲染
# 一帧再存(那样不受桌面环境影响)。这一步等界面稳定了再补。

[CmdletBinding()]
param(
    [string[]]$Pages = @('weather', 'plugins'),
    [switch]$All,
    [string]$OutDir = 'dist\shots',
    [int]$SettleMs = 2600,
    [string]$Exe = 'target\release\nfarms.exe',
    # 启动时打开这张图片(给图片阅读器拍图用)。走的是 --open-image。
    #
    # 为什么不做成通用的 -ExtraArgs 数组:从 `cmd /c` 里传 PowerShell 数组时
    # **逗号会被 cmd 当成分隔符**吃掉(`-ExtraArgs 'a','b'` 会被拆成两个 token),
    # 参数就悄悄丢了 —— 而且不报错,只是截图里没有图。专用参数最省事。
    [string]$OpenImage = '',
    # 启动时同时弹出独立窗口(--popout),并且**拍那个弹出窗口**而不是主窗口。
    # 数字雨这类页面在弹出窗口里才是它的正常形态。
    [switch]$Popout
)

$ErrorActionPreference = 'Continue'
$ws = Split-Path -Parent $PSScriptRoot

Add-Type -AssemblyName System.Drawing
Add-Type @"
using System;
using System.Runtime.InteropServices;
public class NfShot {
  [StructLayout(LayoutKind.Sequential)] public struct RECT { public int Left, Top, Right, Bottom; }
  [DllImport("user32.dll")] public static extern bool GetWindowRect(IntPtr h, out RECT r);
  [DllImport("user32.dll")] public static extern bool SetWindowPos(IntPtr h, IntPtr after, int x, int y, int cx, int cy, uint flags);
  [DllImport("user32.dll")] public static extern bool SetForegroundWindow(IntPtr h);
  [DllImport("user32.dll")] public static extern bool ShowWindow(IntPtr h, int cmd);
  [DllImport("user32.dll")] public static extern bool SetProcessDPIAware();
  public delegate bool EnumProc(IntPtr h, IntPtr p);
  [DllImport("user32.dll")] public static extern bool EnumWindows(EnumProc cb, IntPtr p);
  [DllImport("user32.dll")] public static extern uint GetWindowThreadProcessId(IntPtr h, out uint pid);
  [DllImport("user32.dll", CharSet=CharSet.Unicode)] public static extern int GetWindowTextW(IntPtr h, System.Text.StringBuilder s, int n);
  [DllImport("user32.dll")] public static extern bool IsWindowVisible(IntPtr h);
  // 找出某进程的全部**可见**顶层窗口。MainWindowHandle 只给第一个,
  // 弹出窗口是第二个 —— 只靠它就永远拍不到弹出窗口。
  public static IntPtr[] VisibleWindows(uint want) {
    var list = new System.Collections.Generic.List<IntPtr>();
    EnumWindows((h, p) => {
      uint pid; GetWindowThreadProcessId(h, out pid);
      if (pid == want && IsWindowVisible(h)) list.Add(h);
      return true;
    }, IntPtr.Zero);
    return list.ToArray();
  }
  public static string Title(IntPtr h) {
    var sb = new System.Text.StringBuilder(512);
    GetWindowTextW(h, sb, sb.Capacity);
    return sb.ToString();
  }
}
"@

# **必须**先把本进程标成 DPI 感知。
# 否则在 200% 缩放的屏幕上:窗口真实是 2360×1560,但 DPI 虚拟化让
# GetWindowRect 报 1180×780,CopyFromScreen 就只抓到左上角那四分之一 ——
# 拍出来的图像是「放大两倍又被裁掉一半」,看着像界面有 bug,其实是截图有 bug。
[void][NfShot]::SetProcessDPIAware()

# SetWindowPos 用的常量:HWND_TOPMOST + 不改尺寸(NOZORDER 不能给,要的正是 z 序)
$HWND_TOPMOST = [IntPtr](-1)
$HWND_NOTOPMOST = [IntPtr](-2)
$SWP_NOSIZE = 0x0001
$SWP_SHOWWINDOW = 0x0040

$exe = if ([System.IO.Path]::IsPathRooted($Exe)) { $Exe } else { Join-Path $ws $Exe }
if (-not (Test-Path $exe)) { throw "找不到 $exe —— 先跑 .\scripts\build-release.ps1" }

$outDir = if ([System.IO.Path]::IsPathRooted($OutDir)) { $OutDir } else { Join-Path $ws $OutDir }
New-Item -ItemType Directory -Force -Path $outDir | Out-Null

function Get-PageIds {
    $probe = Join-Path $env:TEMP ("nfarms-pages-" + [guid]::NewGuid().ToString('N').Substring(0, 8) + ".txt")
    cmd /c ('"{0}" --list-pages > "{1}" 2>&1' -f $exe, $probe) | Out-Null
    $text = if (Test-Path $probe) { Get-Content $probe -Encoding UTF8 } else { @() }
    Remove-Item $probe -Force -ErrorAction SilentlyContinue
    $ids = @()
    foreach ($line in $text) {
        if (-not $line -or $line.StartsWith('#')) { continue }
        $ids += ($line -split "`t")[0]
    }
    return $ids
}

if ($All) { $Pages = Get-PageIds }
# 命令行写 `-Pages a,b,c` 时,PowerShell 交给 [string[]] 的其实是**一个**元素
# "a,b,c"(只有 `-Pages a b c` 才会拆成三个)。自己再拆一次,两种写法都认。
$Pages = $Pages | ForEach-Object { $_.Split(',') } | ForEach-Object { $_.Trim() } | Where-Object { $_ }
if (-not $Pages -or $Pages.Count -eq 0) { throw '没有要拍的页' }

Get-Process -Name 'nfarms' -ErrorAction SilentlyContinue | Stop-Process -Force -ErrorAction SilentlyContinue
Start-Sleep -Milliseconds 300

Write-Host ("拍 {0} 页 -> {1}" -f $Pages.Count, $outDir) -ForegroundColor Cyan
foreach ($page in $Pages) {
    $launchArgs = @('--page', $page)
    if ($OpenImage) { $launchArgs += @('--open-image', $OpenImage) }
    if ($Popout) { $launchArgs += '--popout' }
    $proc = Start-Process -FilePath $exe -ArgumentList $launchArgs -PassThru
    Start-Sleep -Milliseconds $SettleMs
    $proc.Refresh()
    # 进程已经退出时 MainWindowHandle 是**空**(不是 0),直接和 IntPtr::Zero
    # 比会漏过去,后面每个 P/Invoke 都报 "Cannot convert null to IntPtr"。
    $h = if ($proc.HasExited) { [IntPtr]::Zero } else { $proc.MainWindowHandle }
    if (-not $h -or $h -eq [IntPtr]::Zero) {
        Write-Host ("  {0,-18} 拿不到窗口句柄(进程是不是已经退出了?)" -f $page) -ForegroundColor Red
        Stop-Process -Id $proc.Id -Force -ErrorAction SilentlyContinue
        continue
    }

    # -Popout:要拍的是**第二个**可见窗口(标题带破折号的那个)。
    # MainWindowHandle 永远只给第一个,所以这里得自己枚举。
    if ($Popout) {
        $wins = [NfShot]::VisibleWindows([uint32]$proc.Id) | Where-Object { [NfShot]::Title($_) -like '*—*' }
        if ($wins -and $wins.Count -gt 0) { $h = $wins[0] }
        else {
            Write-Host ("  {0,-18} 没找到弹出窗口,退回主窗口" -f $page) -ForegroundColor Yellow
        }
    }

    # 挪到 (0,0) 并置顶 —— 不然抓到的是压在上面的窗口
    [void][NfShot]::ShowWindow($h, 5)              # SW_SHOW
    [void][NfShot]::SetWindowPos($h, $HWND_TOPMOST, 0, 0, 0, 0, $SWP_NOSIZE)
    [void][NfShot]::SetForegroundWindow($h)
    Start-Sleep -Milliseconds 900

    $r = New-Object NfShot+RECT
    [void][NfShot]::GetWindowRect($h, [ref]$r)
    $w = $r.Right - $r.Left
    $ht = $r.Bottom - $r.Top
    if ($w -le 0 -or $ht -le 0) {
        Write-Host ("  {0,-18} 窗口矩形不合法" -f $page) -ForegroundColor Red
        Stop-Process -Id $proc.Id -Force -ErrorAction SilentlyContinue
        continue
    }

    $bmp = New-Object System.Drawing.Bitmap($w, $ht)
    $g = [System.Drawing.Graphics]::FromImage($bmp)
    $g.CopyFromScreen($r.Left, $r.Top, 0, 0, $bmp.Size)
    $name = ($page -replace '[^A-Za-z0-9.-]', '_')
    $file = Join-Path $outDir ("ui-{0}.png" -f $name)
    $bmp.Save($file, [System.Drawing.Imaging.ImageFormat]::Png)
    $g.Dispose()
    $bmp.Dispose()

    [void][NfShot]::SetWindowPos($h, $HWND_NOTOPMOST, 0, 0, 0, 0, $SWP_NOSIZE)
    Write-Host ("  {0,-18} -> {1}  ({2}×{3})" -f $page, (Split-Path -Leaf $file), $w, $ht) -ForegroundColor DarkGreen
    Stop-Process -Id $proc.Id -Force -ErrorAction SilentlyContinue
    Start-Sleep -Milliseconds 400
}
Write-Host ''
Get-ChildItem $outDir -Filter 'ui-*.png' | Sort-Object Name | ForEach-Object {
    "  {0,-26} {1,8:N0} KB" -f $_.Name, ($_.Length / 1KB)
}