MENU

スケジューラ最新版

# MiniScheduler3.ps1

Add-Type -AssemblyName System.Windows.Forms
Add-Type -AssemblyName System.Drawing
[System.Windows.Forms.Application]::EnableVisualStyles()

# ===== 設定 =====
$TrayOnlyWhenMinimized = $true
$JobCount = 4              # 枠数
$StartMinimized = $false    # ← 起動時に最小化したい場合は $true, 通常表示なら $false

# ===== パス・ログ =====
$AppDir   = Split-Path -Parent $MyInvocation.MyCommand.Path
$JobsPath = Join-Path $AppDir "jobs3.json"
$LogPath  = Join-Path $AppDir "MiniScheduler3.log"
function Write-Log([string]$msg){ try{ Add-Content -Path $LogPath -Value ("{0} {1}" -f (Get-Date -Format "yyyy-MM-dd HH:mm:ss"),$msg) -Encoding UTF8 }catch{} }

# ===== 表示は日本語・内部コードは英語 =====
$RecurrenceItems = @(
    @{ Text = "なし";       Value = "None" }
    @{ Text = "毎日";       Value = "Daily" }
    @{ Text = "毎週";       Value = "Weekly" }
    @{ Text = "毎月1日";    Value = "Monthly1st" }
    @{ Text = "毎月指定日"; Value = "MonthlyDay" }
)
function Init-RecurrenceCombo([System.Windows.Forms.ComboBox]$cb){
    $cb.DropDownStyle = "DropDownList"
    $cb.DisplayMember = "Text"
    $cb.ValueMember   = "Value"
    foreach ($it in $RecurrenceItems){ [void]$cb.Items.Add( (New-Object PSObject -Property $it) ) }
}
function Set-ComboByValue([System.Windows.Forms.ComboBox]$cb,[string]$val){
    foreach($item in $cb.Items){ if($item.Value -eq $val){ $cb.SelectedItem=$item; return } }
    foreach($item in $cb.Items){ if($item.Value -eq "None"){ $cb.SelectedItem=$item; break } }
}
function Get-ComboValue([System.Windows.Forms.ComboBox]$cb){
    if($cb.SelectedItem -and $cb.SelectedItem.PSObject.Properties.Match('Value').Count -gt 0){ return [string]$cb.SelectedItem.Value }
    return "None"
}

# ===== タスクトレイ =====
$script:tray = New-Object System.Windows.Forms.NotifyIcon
$script:tray.Icon = [System.Drawing.SystemIcons]::Application
$script:tray.Text = "ミニスケジューラー"
$script:tray.Visible = (-not $TrayOnlyWhenMinimized)
$cms = New-Object System.Windows.Forms.ContextMenuStrip
$miOpen = $cms.Items.Add("開く")
$miExit = $cms.Items.Add("終了")
$script:tray.ContextMenuStrip = $cms

# ===== モデル =====
function New-EmptyJob{
  [ordered]@{ Enabled=$false; Path=""; RunAt=(Get-Date); Recurrence="None"; DayOfMonth=1; LastRun=""; Status="" }
}
$Jobs = @()
1..$JobCount | ForEach-Object { $Jobs += New-EmptyJob }

# --- 読込(RunAtはISO8601) ---
if(Test-Path $JobsPath){
  try{
    $loaded = Get-Content $JobsPath -Raw | ConvertFrom-Json
    for($i=0;$i -lt [Math]::Min($JobCount,$loaded.Count);$i++){
      $Jobs[$i] = $loaded[$i]
      if($Jobs[$i].RunAt){
        try{
          if($Jobs[$i].RunAt -is [string]){ $Jobs[$i].RunAt = [datetime]::ParseExact($Jobs[$i].RunAt,"o",$null) }
        }catch{ $Jobs[$i].RunAt = Get-Date }
      } else { $Jobs[$i].RunAt = Get-Date }
      if(-not $Jobs[$i].Recurrence){ $Jobs[$i].Recurrence="None" }
      if(-not $Jobs[$i].DayOfMonth){ $Jobs[$i].DayOfMonth=1 }
    }
  }catch{ Write-Log "設定読込エラー: $($_.Exception.Message)" }
}
# --- 保存(RunAtはISO8601) ---
function Save-Jobs{
  $arr = for($i=0;$i -lt $JobCount;$i++){
    $j=$Jobs[$i]
    New-Object psobject -Property ([ordered]@{
      Enabled=[bool]$j.Enabled; Path=[string]$j.Path
      RunAt=([datetime]$j.RunAt).ToString("o")
      Recurrence=[string]$j.Recurrence; DayOfMonth=[int]$j.DayOfMonth
      LastRun=[string]$j.LastRun; Status=[string]$j.Status
    })
  }
  try{ ($arr|ConvertTo-Json -Depth 6) | Set-Content -Path $JobsPath -Encoding UTF8 }catch{ Write-Log "設定保存エラー: $($_.Exception.Message)" }
}

# ===== 日付計算(週末→月曜シフト対応) =====
function Get-EndOfMonth([datetime]$d){ (Get-Date -Year $d.Year -Month $d.Month -Day 1).AddMonths(1).AddDays(-1) }
function Shift-WeekendToMonday([datetime]$d){
  switch([int]$d.DayOfWeek){
    6 { return $d.AddDays(2) }  # Sat -> Mon
    0 { return $d.AddDays(1) }  # Sun -> Mon
    default { return $d }
  }
}
function Get-NextOccurrence([string]$rule,[datetime]$runAt,[int]$day){
  $now = Get-Date
  $t = Get-Date -Hour $runAt.Hour -Minute $runAt.Minute -Second $runAt.Second -Millisecond 0
  switch($rule){
    "Daily" {
      $c = Get-Date -Year $now.Year -Month $now.Month -Day $now.Day -Hour $t.Hour -Minute $t.Minute -Second $t.Second
      if($c -le $now){ $c = $c.AddDays(1) }
      return (Shift-WeekendToMonday $c)
    }
    "Weekly" {
      $target = [int]$runAt.DayOfWeek
      $c = Get-Date -Year $now.Year -Month $now.Month -Day $now.Day -Hour $t.Hour -Minute $t.Minute -Second $t.Second
      while(([int]$c.DayOfWeek -ne $target) -or ($c -le $now)){ $c=$c.AddDays(1) }
      return (Shift-WeekendToMonday $c)
    }
    "Monthly1st" {
      $base = Get-Date -Year $now.Year -Month $now.Month -Day 1 -Hour $t.Hour -Minute $t.Minute -Second $t.Second
      $c = if($base -le $now){ $base.AddMonths(1) } else { $base }
      return (Shift-WeekendToMonday $c)
    }
    "MonthlyDay" {
      $d = [Math]::Min($day,(Get-EndOfMonth $now).Day)
      $base = Get-Date -Year $now.Year -Month $now.Month -Day $d -Hour $t.Hour -Minute $t.Minute -Second $t.Second
      if($base -le $now){
        $m = $now.AddMonths(1)
        $d2 = [Math]::Min($day,(Get-EndOfMonth $m).Day)
        $base = Get-Date -Year $m.Year -Month $m.Month -Day $d2 -Hour $t.Hour -Minute $t.Minute -Second $t.Second
      }
      return (Shift-WeekendToMonday $base)
    }
    default {  # None
      $c = if($runAt -le $now){ $now.AddMinutes(1) } else { $runAt }
      return (Shift-WeekendToMonday $c)
    }
  }
}

# ===== UI(DPIレイアウト:横並び)=====
$form = New-Object System.Windows.Forms.Form
$form.Text="ミニスケジューラー(8枠)ver11"
$form.StartPosition="CenterScreen"
$form.MinimumSize=New-Object System.Drawing.Size(1100,680)
$form.AutoScaleMode=[System.Windows.Forms.AutoScaleMode]::Dpi

# 列構成:
# 0: プログラム名(テキストボックス)  [Percent 100]
# 1: 参照ボタン                          [Auto]
# 2: 有効(チェック)                     [Auto]
# 3: 実行日時(DateTimePicker)          [Auto]
# 4: 今すぐ実行(ボタン)                 [Auto]
# 5: 自動設定(ボタン)                   [Auto]
# 6: 次回(ラベル)                       [Auto]

$tbl = New-Object System.Windows.Forms.TableLayoutPanel
$tbl.Dock="Fill"
$tbl.Padding=New-Object System.Windows.Forms.Padding(8)
$tbl.AutoSize=$true
$tbl.AutoScroll=$true
$tbl.ColumnCount = 9
$tbl.ColumnStyles.Clear()
$tbl.ColumnStyles.Add((New-Object System.Windows.Forms.ColumnStyle([System.Windows.Forms.SizeType]::Absolute,200))) # 0:プログラム名
1..8 | ForEach-Object {
    $tbl.ColumnStyles.Add((New-Object System.Windows.Forms.ColumnStyle([System.Windows.Forms.SizeType]::AutoSize)))
}

# ── ヘッダー行 ──
$headers = @("プログラム名","参照ボタン","有効","くり返し","日","実行日時","今すぐ実行","自動設定","次回")

for($c=0;$c -lt $headers.Count;$c++){
  $h = New-Object System.Windows.Forms.Label
  $h.Text = $headers[$c]
  $h.AutoSize = $true
  $h.Font = New-Object System.Drawing.Font($h.Font, [System.Drawing.FontStyle]::Bold)
  $h.Margin = New-Object System.Windows.Forms.Padding(3,3,6,6)
  $tbl.Controls.Add($h,$c,0)
}

# コントロール配列(既存の変数名を維持)
$txtPath=@(); $btnBrowse=@(); $chkEnable=@(); $dtRun=@(); $btnRunNow=@(); $lblStatus=@(); $cbRule=@(); $nudDay=@(); $btnAuto=@()

# ── 各ジョブ1行で横並び ──
for($i=0;$i -lt $JobCount;$i++){
  $row = $i + 1
  $tbl.RowStyles.Add( (New-Object System.Windows.Forms.RowStyle([System.Windows.Forms.SizeType]::AutoSize)) )

  # 0: プログラム名
  $link = New-Object System.Windows.Forms.LinkLabel
  $link.AutoSize = $true
  $link.Margin = New-Object System.Windows.Forms.Padding(3,5,6,6)
  $tbl.Controls.Add($link,0,$row)
  $txtPath += $link

  # 1: 参照ボタン
  $b = New-Object System.Windows.Forms.Button
  $b.Text = "参照..."
  $b.AutoSize = $true
  $b.Margin = New-Object System.Windows.Forms.Padding(0,0,6,6)
  $tbl.Controls.Add($b,1,$row)
  $btnBrowse += $b

  # 2: 有効
  $c = New-Object System.Windows.Forms.CheckBox
  $c.Text = "有効"
  $c.AutoSize = $true
  $c.Margin = New-Object System.Windows.Forms.Padding(0,5,6,6)
  $tbl.Controls.Add($c,2,$row)
  $chkEnable += $c

  # 3: くり返し(コンボ)
  $cb = New-Object System.Windows.Forms.ComboBox
  Init-RecurrenceCombo $cb
  $cb.DropDownWidth = 120
  $cb.Width = 110
  $cb.Margin = New-Object System.Windows.Forms.Padding(0,0,6,6)
  $tbl.Controls.Add($cb,3,$row)
  $cbRule += $cb

  # 4: 日(NumericUpDown)
  $nud = New-Object System.Windows.Forms.NumericUpDown
  $nud.Minimum = 1; $nud.Maximum = 31
  $nud.Width = 60
  $nud.Margin = New-Object System.Windows.Forms.Padding(0,0,6,6)
  $tbl.Controls.Add($nud,4,$row)
  $nudDay += $nud

  # 5: 実行日時
  $dt = New-Object System.Windows.Forms.DateTimePicker
  $dt.Format = "Custom"
  $dt.CustomFormat = "yyyy/MM/dd HH:mm:ss"
  $dt.Width = 180
  $dt.Margin = New-Object System.Windows.Forms.Padding(0,0,6,6)
  $tbl.Controls.Add($dt,5,$row)
  $dtRun += $dt

  # 6: 今すぐ実行
  $bn = New-Object System.Windows.Forms.Button
  $bn.Text = "今すぐ実行"
  $bn.AutoSize = $true
  $bn.Margin = New-Object System.Windows.Forms.Padding(0,0,6,6)
  $tbl.Controls.Add($bn,6,$row)
  $btnRunNow += $bn

  # 7: 自動設定
  $btnA = New-Object System.Windows.Forms.Button
  $btnA.Text = "自動設定"
  $btnA.AutoSize = $true
  $btnA.Margin = New-Object System.Windows.Forms.Padding(0,0,6,6)
  $tbl.Controls.Add($btnA,7,$row)
  $btnAuto += $btnA

  # 8: 次回(ステータス)
  $st = New-Object System.Windows.Forms.Label
  $st.AutoSize = $true
  $st.Margin = New-Object System.Windows.Forms.Padding(0,5,6,6)
  $tbl.Controls.Add($st,8,$row)
  $lblStatus += $st

}


# ===== 下部ボタン =====
$footerRow = $JobCount + 1   # ヘッダー1行ぶんオフセット
$tbl.RowStyles.Add( (New-Object System.Windows.Forms.RowStyle([System.Windows.Forms.SizeType]::AutoSize)) )

$btnAutoAll = New-Object System.Windows.Forms.Button; $btnAutoAll.Text="全てを自動設定"
$btnAutoAll.Width = 100
$btnSave    = New-Object System.Windows.Forms.Button; $btnSave.Text="保存"
$btnExit    = New-Object System.Windows.Forms.Button; $btnExit.Text="終了"

$pnl = New-Object System.Windows.Forms.FlowLayoutPanel
$pnl.FlowDirection = 'LeftToRight'
$pnl.AutoSize = $true
$pnl.Controls.Add($btnAutoAll); $pnl.Controls.Add($btnSave); $pnl.Controls.Add($btnExit)

$tbl.Controls.Add($pnl,0,$footerRow)
$tbl.SetColumnSpan($pnl,7)

$form.Controls.Add($tbl)

# ===== トレイ格納関数 =====
function Minimize-ToTray {
    $form.WindowState = 'Minimized'
    $form.ShowInTaskbar = $false
    $script:tray.Visible = $true
}

$restore = {
    $form.WindowState = 'Normal'
    $form.ShowInTaskbar = $true
    if ($TrayOnlyWhenMinimized) { $script:tray.Visible = $false }
    $form.Activate()
}

$miOpen.add_Click($restore)
$script:tray.add_DoubleClick($restore)

$miExit.add_Click({
    $script:tray.Visible = $false
    $script:tray.Dispose()
    $form.Close()
})

# ===== 既存値→UI =====
for($i=0;$i -lt $JobCount;$i++){
  $full = [string]$Jobs[$i].Path
  $name = if([string]::IsNullOrWhiteSpace($full)) { "" } else { Split-Path $full -Leaf }

  # LinkLabel に表示はファイル名だけ、フルパスは Tag へ
  $txtPath[$i].Text  = $name
  $txtPath[$i].Tag   = $full
  # 任意: フルパスをツールチップで見せたい場合
  # $tip = New-Object System.Windows.Forms.ToolTip
  # $tip.SetToolTip($txtPath[$i], $full)

  $chkEnable[$i].Checked = [bool]$Jobs[$i].Enabled
  $dtRun[$i].Value       = [datetime]$Jobs[$i].RunAt
  $lblStatus[$i].Text    = [string]$Jobs[$i].Status
  Set-ComboByValue $cbRule[$i] ([string]$Jobs[$i].Recurrence)
  $nudDay[$i].Value      = [int]$Jobs[$i].DayOfMonth
}


# ===== 共通関数:1枠を自動設定(UIの「くり返し」「日」を参照)=====
function Apply-Auto([int]$idx){
  # UIの値をそのまま採用
  $rule = Get-ComboValue $cbRule[$idx]           # なし/毎日/毎週/毎月1日/毎月指定日
  $day  = [int]$nudDay[$idx].Value               # 「日」(1-31) ※MonthlyDayのときだけ有効
  $base = [datetime]$dtRun[$idx].Value           # 基準の時刻(時分秒を使う)

  # 次回を計算(週末→月曜シフトは中で処理)
  $next = Get-NextOccurrence $rule $base $day

  # 反映
  $dtRun[$idx].Value     = $next
  $Jobs[$idx].RunAt      = $next
  $Jobs[$idx].Recurrence = $rule
  $Jobs[$idx].DayOfMonth = $day

  $Jobs[$idx].Status     = "次回: " + $next.ToString("yyyy/MM/dd HH:mm:ss")
  $lblStatus[$idx].Text  = $Jobs[$idx].Status
}


# ===== 参照/今すぐ/自動設定/全自動 =====
for($i=0;$i -lt $JobCount;$i++){ $btnBrowse[$i].Tag=$i; $btnRunNow[$i].Tag=$i; $btnAuto[$i].Tag=$i }

foreach($b in $btnBrowse){
  $b.Add_Click({
    param($s,$e)
    $i=[int]$s.Tag
    $ofd=New-Object System.Windows.Forms.OpenFileDialog
    $ofd.Filter="すべてのファイル|*.*"
    if($ofd.ShowDialog() -eq [System.Windows.Forms.DialogResult]::OK){
      $full = $ofd.FileName
      $name = Split-Path $full -Leaf
      $txtPath[$i].Text = $name
      $txtPath[$i].Tag  = $full
      $txtPath[$i].LinkColor = "Blue"
    }
  })
}

foreach($link in $txtPath){
  $link.Add_LinkClicked({
    param($s,$e)
    $path = $s.Tag
    if([string]::IsNullOrWhiteSpace($path)){ return }
    if(Test-Path $path){ Start-Process $path }
    else{ [System.Windows.Forms.MessageBox]::Show("ファイルが見つかりません。") | Out-Null }
  })
}

function Try-UnblockFile([string]$path) {
  try {
    if (Test-Path -LiteralPath $path) {
      Unblock-File -LiteralPath $path -ErrorAction Stop
    }
  } catch { }  # 失敗は無視
}


function Invoke-Path([string]$p){
  try{
    Try-UnblockFile $p
    $psi = New-Object System.Diagnostics.ProcessStartInfo
    $psi.FileName = $p
    $psi.UseShellExecute = $true   # 既定の関連付けで開く
    [System.Diagnostics.Process]::Start($psi) | Out-Null
    return $true,""
  } catch {
    return $false,$_.Exception.Message
  }
}

foreach($b in $btnRunNow){
  $b.Add_Click({
    param($s,$e)
    $i=[int]$s.Tag
    $p = [string]$txtPath[$i].Tag
    if([string]::IsNullOrWhiteSpace($p)){
      [System.Windows.Forms.MessageBox]::Show("パスを入力してください。")|Out-Null; return
    }  # ← この } が必要
    $ok,$err=Invoke-Path $p
    $Jobs[$i].LastRun=(Get-Date).ToString("yyyy/MM/dd HH:mm:ss")
    if($ok){
      if($Jobs[$i].Recurrence -eq "None"){ $Jobs[$i].Status="実行完了"; $Jobs[$i].Enabled=$false; $chkEnable[$i].Checked=$false }
      else{
        $next = Get-NextOccurrence $Jobs[$i].Recurrence $Jobs[$i].RunAt ([int]$Jobs[$i].DayOfMonth)
        $Jobs[$i].RunAt=$next; $dtRun[$i].Value=$next
        $Jobs[$i].Status="実行完了 → 次回: "+$next.ToString("yyyy/MM/dd HH:mm:ss")
      }
    } else { [System.Windows.Forms.MessageBox]::Show("実行エラー:`n"+$err)|Out-Null; $Jobs[$i].Status="実行エラー" }
    Save-Jobs; $lblStatus[$i].Text=$Jobs[$i].Status

    # ← 実行後に最小化してトレイへ
    Minimize-ToTray
  })
}

foreach($b in $btnAuto){
  $b.Add_Click({
    param($s,$e)
    $i=[int]$s.Tag
    Apply-Auto $i
    Save-Jobs
    [System.Windows.Forms.MessageBox]::Show("プログラム{0} を {1} に設定しました。".Replace("{0}",($i+1)).Replace("{1}",$Jobs[$i].RunAt.ToString("yyyy/MM/dd HH:mm:ss"))) | Out-Null
  })
}

$btnAutoAll.Add_Click({
  for($i=0;$i -lt $JobCount;$i++){ Apply-Auto $i }
  Save-Jobs
  [System.Windows.Forms.MessageBox]::Show(("{0}件すべてのスケジュールを自動設定しました。(土日は月曜へシフト)" -f $JobCount))|Out-Null
})

# ===== 保存・終了 =====
$btnSave.Add_Click({
  for($i=0;$i -lt $JobCount;$i++){
    $Jobs[$i].Path=[string]$txtPath[$i].Tag   # ← ここを Text から Tag に
    $Jobs[$i].Enabled=$chkEnable[$i].Checked
    $Jobs[$i].RunAt=$dtRun[$i].Value
    $Jobs[$i].Recurrence = Get-ComboValue $cbRule[$i]
    $Jobs[$i].DayOfMonth=[int]$nudDay[$i].Value
  }
  Save-Jobs
  [System.Windows.Forms.MessageBox]::Show("保存しました。")|Out-Null
})
$btnExit.Add_Click({ $script:tray.Visible=$false; $script:tray.Dispose(); $form.Close() })

# ===== タイマー(1秒)=====
$timer = New-Object System.Windows.Forms.Timer
$timer.Interval = 1000
$timer.Add_Tick({
  $now = Get-Date
  for($i=0; $i -lt $JobCount; $i++){
    try{
      if(-not $Jobs[$i].Enabled){ continue }

      # まだ実行時刻に達していないなら次へ
      if($now -lt [datetime]$Jobs[$i].RunAt){ continue }

      # パス確認
      $p = [string]$Jobs[$i].Path
      if([string]::IsNullOrWhiteSpace($p) -or -not (Test-Path -LiteralPath $p)){
        $Jobs[$i].Status   = "パス未設定"
        $Jobs[$i].Enabled  = $false
        $chkEnable[$i].Checked = $false
        Save-Jobs
        $lblStatus[$i].Text = $Jobs[$i].Status
        continue
      }

      # 実行
      $ok,$err = Invoke-Path $p
      $Jobs[$i].LastRun = (Get-Date).ToString("yyyy/MM/dd HH:mm:ss")

      if($ok){
        # 実行成功:常に無効化(ユーザーが再度有効にする前提)
        $Jobs[$i].Enabled = $false
        $chkEnable[$i].Checked = $false

        if($Jobs[$i].Recurrence -eq "None"){
          $Jobs[$i].Status = "実行完了"
        } else {
          # 次回は計算してUIに表示だけ更新(無効なので次は走らない)
          $next = Get-NextOccurrence $Jobs[$i].Recurrence ([datetime]$Jobs[$i].RunAt) ([int]$Jobs[$i].DayOfMonth)
          $Jobs[$i].RunAt = $next
          $dtRun[$i].Value = $next
          $Jobs[$i].Status = "実行完了 → 次回: " + $next.ToString("yyyy/MM/dd HH:mm:ss")
        }

        Save-Jobs
        $lblStatus[$i].Text = $Jobs[$i].Status

        # 成功時のみ最小化してトレイへ
        Minimize-ToTray

      } else {
        # 実行エラー:無効化して状態を表示。最小化はしない
        $Jobs[$i].Status  = "実行エラー"
        $Jobs[$i].Enabled = $false
        $chkEnable[$i].Checked = $false
        Save-Jobs
        $lblStatus[$i].Text = $Jobs[$i].Status
      }
    }
    catch {
      # 念のためのガード:予期せぬ例外時も無効化
      $Jobs[$i].Status  = "内部エラー"
      $Jobs[$i].Enabled = $false
      $chkEnable[$i].Checked = $false
      Save-Jobs
      $lblStatus[$i].Text = $Jobs[$i].Status
    }
  }
})
$timer.Start()



# ===== 起動時の動作(最小化オプション対応) =====
if ($StartMinimized) {
    $form.Add_Shown({ Minimize-ToTray })
}

# ===== 表示 =====
[System.Windows.Forms.Application]::Run($form)
よかったらシェアしてね!
  • URLをコピーしました!

この記事を書いた人

コメント

コメントする

日本語が含まれない投稿は無視されますのでご注意ください。(スパム対策)

目次