# MiniScheduler3.ps1 - 8枠版(日本語UI / 繰り返し / 全自動設定 / 週末→月曜シフト / 実行後に最小化)
# ・既定の関連付けで開く(ダブルクリック相当)
# ・最小化中のみタスクトレイ表示、×で終了
# ・RunAt は ISO8601 で保存、読込は厳密復元(ParseExact "o")
# ・繰り返し: なし/毎日/毎週/毎月1日/毎月指定日
# ・[自動設定] と [全てを自動設定] は “今より未来” に合わせ、土日なら翌月曜へ
# ・表示は日本語、内部コードは英語(None/Daily/Weekly/Monthly1st/MonthlyDay)を保持
# ・手動実行・自動実行のいずれも、実行後に最小化してトレイへ格納
# ・起動時最小化はオプション化($StartMinimized)
Add-Type -AssemblyName System.Windows.Forms
Add-Type -AssemblyName System.Drawing
[System.Windows.Forms.Application]::EnableVisualStyles()
# ===== 設定 =====
$TrayOnlyWhenMinimized = $true
$JobCount = 8 # 枠数
$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
$tbl = New-Object System.Windows.Forms.TableLayoutPanel
$tbl.Dock="Fill"; $tbl.ColumnCount=9
$tbl.Padding=New-Object System.Windows.Forms.Padding(8)
$tbl.AutoSize=$true; $tbl.AutoScroll=$true
$tbl.ColumnStyles.Add((New-Object System.Windows.Forms.ColumnStyle([System.Windows.Forms.SizeType]::AutoSize)))
$tbl.ColumnStyles.Add((New-Object System.Windows.Forms.ColumnStyle([System.Windows.Forms.SizeType]::Percent,100)))
$tbl.ColumnStyles.Add((New-Object System.Windows.Forms.ColumnStyle([System.Windows.Forms.SizeType]::AutoSize)))
$tbl.ColumnStyles.Add((New-Object System.Windows.Forms.ColumnStyle([System.Windows.Forms.SizeType]::AutoSize)))
$tbl.ColumnStyles.Add((New-Object System.Windows.Forms.ColumnStyle([System.Windows.Forms.SizeType]::AutoSize)))
$tbl.ColumnStyles.Add((New-Object System.Windows.Forms.ColumnStyle([System.Windows.Forms.SizeType]::AutoSize)))
$tbl.ColumnStyles.Add((New-Object System.Windows.Forms.ColumnStyle([System.Windows.Forms.SizeType]::AutoSize)))
$tbl.ColumnStyles.Add((New-Object System.Windows.Forms.ColumnStyle([System.Windows.Forms.SizeType]::AutoSize)))
$tbl.ColumnStyles.Add((New-Object System.Windows.Forms.ColumnStyle([System.Windows.Forms.SizeType]::AutoSize)))
$txtPath=@(); $btnBrowse=@(); $chkEnable=@(); $dtRun=@(); $btnRunNow=@(); $lblStatus=@(); $cbRule=@(); $nudDay=@(); $btnAuto=@()
for($i=0;$i -lt $JobCount;$i++){
$row = $i*3
$tbl.RowStyles.Add((New-Object System.Windows.Forms.RowStyle([System.Windows.Forms.SizeType]::AutoSize)))
$tbl.RowStyles.Add((New-Object System.Windows.Forms.RowStyle([System.Windows.Forms.SizeType]::AutoSize)))
$tbl.RowStyles.Add((New-Object System.Windows.Forms.RowStyle([System.Windows.Forms.SizeType]::AutoSize)))
$lbl=New-Object System.Windows.Forms.Label; $lbl.Text="プログラム{0}:" -f ($i+1); $lbl.AutoSize=$true
$tbl.Controls.Add($lbl,0,$row)
$t=New-Object System.Windows.Forms.TextBox; $t.Anchor="Left,Right"; $t.Width=620
$tbl.Controls.Add($t,1,$row); $txtPath+=$t
$b=New-Object System.Windows.Forms.Button; $b.Text="参照..."
$tbl.Controls.Add($b,2,$row); $btnBrowse+=$b
$c=New-Object System.Windows.Forms.CheckBox; $c.Text="有効"
$tbl.Controls.Add($c,3,$row); $chkEnable+=$c
$bn=New-Object System.Windows.Forms.Button; $bn.Text="今すぐ実行"
$tbl.Controls.Add($bn,4,$row); $btnRunNow+=$bn
$st=New-Object System.Windows.Forms.Label; $st.AutoSize=$true
$tbl.Controls.Add($st,5,$row); $lblStatus+=$st
$l2=New-Object System.Windows.Forms.Label; $l2.Text="実行日時:"; $l2.AutoSize=$true
$tbl.Controls.Add($l2,0,$row+1)
$dt=New-Object System.Windows.Forms.DateTimePicker; $dt.Format="Custom"; $dt.CustomFormat="yyyy/MM/dd HH:mm:ss"; $dt.Width=200
$tbl.SetColumnSpan($dt,2); $tbl.Controls.Add($dt,1,$row+1); $dtRun+=$dt
$btnA=New-Object System.Windows.Forms.Button; $btnA.Text="自動設定"
$tbl.Controls.Add($btnA,4,$row+1); $btnAuto+=$btnA
$l3=New-Object System.Windows.Forms.Label; $l3.Text="くり返し:"; $l3.AutoSize=$true
$tbl.Controls.Add($l3,0,$row+2)
$cb=New-Object System.Windows.Forms.ComboBox
Init-RecurrenceCombo $cb
$tbl.Controls.Add($cb,1,$row+2); $cbRule+=$cb
$l4=New-Object System.Windows.Forms.Label; $l4.Text="日:"; $l4.AutoSize=$true
$tbl.Controls.Add($l4,2,$row+2)
$nud=New-Object System.Windows.Forms.NumericUpDown; $nud.Minimum=1; $nud.Maximum=31; $nud.Width=60
$tbl.Controls.Add($nud,3,$row+2); $nudDay+=$nud
}
# 下部ボタン
$footerRow = $JobCount * 3
$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,9)
$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++){
$txtPath[$i].Text=[string]$Jobs[$i].Path
$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枠を自動設定 =====
function Apply-Auto([int]$idx){
$rule = Get-ComboValue $cbRule[$idx]
$next = Get-NextOccurrence $rule $dtRun[$idx].Value ([int]$nudDay[$idx].Value)
$dtRun[$idx].Value=$next
$Jobs[$idx].RunAt=$next
$Jobs[$idx].Recurrence=$rule
$Jobs[$idx].DayOfMonth=[int]$nudDay[$idx].Value
# ★ Enabled の設定をしない(有効化チェックは触らない)
# $Jobs[$idx].Enabled=$true
# $chkEnable[$idx].Checked=$true
$Jobs[$idx].Status="次回: "+$next.ToString("yyyy/MM/dd HH:mm:ss")
$lblStatus[$idx].Text=$Jobs[$idx].Status
}
function Try-UnblockFile([string]$path) {
try {
if (Test-Path -LiteralPath $path) {
Unblock-File -LiteralPath $path -ErrorAction Stop
}
} catch { } # 失敗しても無視(権限や既に解除済みなど)
}
# ===== 参照/今すぐ/自動設定/全自動 =====
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){ $txtPath[$i].Text=$ofd.FileName }
})
}
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=$txtPath[$i].Text
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=$txtPath[$i].Text
$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++){
if(-not $Jobs[$i].Enabled){ continue }
if($now -ge [datetime]$Jobs[$i].RunAt){
$p=[string]$Jobs[$i].Path
if([string]::IsNullOrWhiteSpace($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){
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 { $Jobs[$i].Status="実行エラー" }
Save-Jobs; $lblStatus[$i].Text=$Jobs[$i].Status
# ← 自動実行後も最小化してトレイへ
Minimize-ToTray
}
}
})
$timer.Start()
# ===== 起動時の動作(最小化オプション対応) =====
if ($StartMinimized) {
$form.Add_Shown({ Minimize-ToTray })
}
# ===== 表示 =====
[System.Windows.Forms.Application]::Run($form)
目次
コメント