PowerShell Memo

このサイトはPowerShell(MSH/Monad)奮闘記です

管理人「newpops吉岡洋」が
「PowerShell(旧名:MSH/Monad)」の研究結果を日々綴っていきます。

【お知らせ】
この日記からPowerShellのTipsを抽出し「PowerShell FAQ」として整理しました。


2011-12-12

[] サーバールームの室温監視を行う <PowerShell Advent Calendar 2011>

はじめに

こんばんは。「PowerShell Memo」管理人のnewpopsこと吉岡洋です。

この度、牟田口さんが企画された「PowerShell Advent Calendar 2011」に参加することになりました。

本イベントの12日目を務めさせていただきます。

今回紹介するのは「サーバールームの室温監視を行う」スクリプトです。

作成の経緯

今から3年前の夏、とあるサーバルームのエアコンの温度センサーの不正動作で「時々、設定温度通りに室温が下がらない」という事象に遭遇しました。この「時々」というのが曲者で、通常は22℃前後に保たれているのですが、週に1回程度、センサーが温度を正しく検知できずに、温度が上昇してしまうのです。

気づくのが遅れると、室温が30℃に上昇することもあるとのこと。夜に問題が起こると、朝まで気づかないので厄介ですね。


運用責任者の方いわく、エアコン業者が調査しても原因が分からないとのことで、室温の監視ができないかと相談を受けました。


・・・ということで、PowerShellで室温の監視スクリプトを作る事にしました。

室温監視スクリプトの要件

  • 室温の閾値(許容上限値)を設定できる
  • サーバールームの室温を定期的に取得する
  • 取得した温度をメールのサブジェクトに設定して、管理者に通知する
  • 閾値超過に関係なく必ずメールを送る(スクリプトが稼働している確証が欲しい)
  • 閾値温度を超えている場合は、「警報メール」を管理者に通知する
  • 室温の取得に失敗した場合は「取得失敗メール」を管理者に通知する
  • 室温を定期的にCSVに出力する
  • 設定はファイルで定義できること
  • 動作試験が容易であること

ハードウェア調達

まずは温度計ですが、ストロベリー・リナックス社の「USB温度・湿度計モジュール」を調達。


この製品は外部から温度モジュールにアクセスするためのDLL(USBMeter.dll)が付属しています。

また、組み立て済みの完成品で4980円と値段もお手頃です。

スクリプト作成におけるポイントと解説

本投稿の後方に掲載したスクリプトについて簡単に解説します。

定義ファイルはXML

今回、室温の閾値やメールの宛先など多くのものを定義ファイルに記述することになります。

私は、PowerShellでツールを作成する場合、定義ファイルは必ずXMLファイルにしています。

理由はPowerShellXMLファイルへのアクセスが非常に容易であるからです。

$xml = [xml](Get-Content ./config.xml)
$value = $xml.xxx.yyy.zzz

このような簡単な記述によりXMLの内容を「System.Xml.XmlDocument」型で取得し、値にアクセスできます。

Windows Native DLLへのアクセス

温度系モジュールに付属している「USBMeter.dll」は.Netで作成されたDLLではなく、C++で作成されたWindows Native DLLです。

PowerShellからWindows Native DLLにアクセスする方法はいくつかありますが、本スクリプトでは、USBMeter.dllへのアクセッサをVB.Netで記述し、VBCodeProviderクラスを用いて動的コンパイルするアプローチを採用しています。

動的コンパイルで得たアセンブリから、GetMethodメソッドで各メソッドへの参照を取得し、Invokeメソッドで実行することができます。

2つの起動モード

スクリプトの動作確認を容易にするために、以下の2つの起動モードを用意し、モード毎に定義ファイルを分けています。

  1. 通常モード → 定義ファイル:config.xml
  2. テストモード → 定義ファイル:config_test.xml

また、起動時のモードの指定方法にはSwitchParameterを利用しています。

SwitchParameterはコマンドレットで多用されている方式で、以下の例では「-Recurse」がSwitchParameterです。

Get-ChildItem -Recurse

SwitchParameterはパラメータ指定の有無で分岐する実装が容易であるため、私は好んで利用しています。

メールの送信

メールの送信には「System.Net.Mail.SmtpClient」クラスを利用します。

ただし、本スクリプトでは、メール送信サーバに認証が不要な場合を想定しています。

もし、認証が必要な場合は、SmtpClientのCredentialsプロパティに適切な設定が必要です。


スクリプト

室温監視スクリプト本体(CheckTemperature.ps1)

   1:  ###############################################################################
   2:  # スクリプト名  :CheckTemperature.ps1
   3:  # 概要          :USB接続型温度計の監視を行う
   4:  #                                                  Powered by Hiroshi Yoshioka
   5:  ###############################################################################
   6:  # 詳細
   7:  #
   8:  #   以下の2つのモードがあり、モード毎に定義ファイルを分けています。
   9:  #     1.通常モード    →  定義ファイル:config.xml
  10:  #     2.テストモード  →  定義ファイル:config_test.xml
  11:  #   動作確認を行う場合は、config_test.xmlを修正し、
  12:  #   テストモードで実行してください。
  13:  #
  14:  # 使用例
  15:  #   1.通常モードで起動する
  16:  #      C:\> ./CheckTemperature.ps1
  17:  #
  18:  #   2.テストモードで起動する
  19:  #      C:\> ./CheckTemperature.ps1 -Test
  20:  # 
  21:  ###############################################################################
  22:   
  23:  Param([Switch]$Test)
  24:   
  25:  # 初期定義
  26:  $CONFIG_DIRNAME       = 'config'
  27:  $CONFIG_FILENAME      = 'config.xml'
  28:  $CONFIG_FILENAME_TEST = 'config_test.xml'
  29:   
  30:  $scriptDir        = Split-Path $MyInvocation.MyCommand.Path -Parent
  31:  $CONFIG_DIR       = Join-Path $scriptDir $CONFIG_DIRNAME
  32:  $CONFIG_PATH      = Join-Path $CONFIG_DIR $CONFIG_FILENAME
  33:  $CONFIG_PATH_TEST = Join-Path $CONFIG_DIR $CONFIG_FILENAME_TEST
  34:  $CHECK_FAULT = -1
  35:   
  36:  # USBMetr.dllが提供する関数を利用可能にする
  37:  $provider = New-Object Microsoft.VisualBasic.VBCodeProvider
  38:  $params = New-Object CodeDom.Compiler.CompilerParameters
  39:  $params.GenerateInMemory = $True
  40:  $source = @'
  41:  Module USBMeter
  42:   
  43:      Public Declare Function GetVers Lib "USBMeter.dll" Alias "_GetVers@4" (ByVal dev As String) As String
  44:      Public Declare Function FindUSB Lib "USBMeter.dll" Alias "_FindUSB@4" (ByRef index As Integer) As String
  45:      Public Declare Function GetTempHumid Lib "USBMeter.dll" Alias "_GetTempHumid@12" (ByVal dev As String, ByRef temp As Double, ByRef humid As Double) As Integer
  46:      Public Declare Function ControlIO Lib "USBMeter.dll" Alias "_ControlIO@12" (ByVal dev As String, ByVal port As Integer, ByVal val_Renamed As Integer) As Integer
  47:      Public Declare Function SetHeater Lib "USBMeter.dll" Alias "_SetHeater@8" (ByVal dev As String, ByVal val_Renamed As Integer) As Integer
  48:      Public Declare Function GetTempHumidTrue Lib "USBMeter.dll" Alias "_GetTempHumidTrue@12" (ByVal dev As String, ByRef temp As Double, ByRef humid As Double) As Integer
  49:   
  50:      Public g_temp As Double
  51:      Public g_humid As Double
  52:   
  53:      Function GetVers_PS(ByVal dev As String) As String
  54:          GetVers_PS = GetVers(dev)
  55:      End Function
  56:   
  57:      Function FindUSB_PS(ByRef index As Integer) As String
  58:          FindUSB_PS = FindUSB(index)
  59:      End Function
  60:   
  61:      Function GetTempHumid_PS(ByVal dev As String) As Integer
  62:          GetTempHumid_PS = GetTempHumid(dev, g_temp, g_humid)
  63:      End Function
  64:   
  65:      Function ControlIO_PS(ByVal dev As String, ByVal port As Integer, ByVal val_Renamed As Integer) As Integer
  66:          ControlIO_PS = ControlIO(dev, port, val_Renamed)
  67:      End Function
  68:   
  69:      Function SetHeater_PS(ByVal dev As String, ByVal val_Renamed As Integer) As Integer
  70:          SetHeater_PS = SetHeater(dev, val_Renamed)
  71:      End Function
  72:   
  73:      Function GetTempHumidTrue_PS(ByVal dev As String) As Integer
  74:          GetTempHumidTrue_PS = GetTempHumidTrue(dev, g_temp, g_humid)
  75:      End Function
  76:   
  77:  End Module
  78:  '@
  79:   
  80:  $compilerResults     = $provider.CompileAssemblyFromSource($params, $source)
  81:  $assembly            = $compilerResults.CompiledAssembly
  82:  $USBMeter            = $assembly.GetType("USBMeter")
  83:   
  84:  # メソッド
  85:  $GetVers_PS          = $USBMeter.GetMethod("GetVers_PS")
  86:  $FindUSB_PS          = $USBMeter.GetMethod("FindUSB_PS")
  87:  $GetTempHumid_PS     = $USBMeter.GetMethod("GetTempHumid_PS")
  88:  $ControlIO_PS        = $USBMeter.GetMethod("ControlIO_PS")
  89:  $SetHeater_PS        = $USBMeter.GetMethod("SetHeater_PS")
  90:  $GetTempHumidTrue_PS = $USBMeter.GetMethod("GetTempHumidTrue_PS")
  91:   
  92:  #############################################################
  93:  # 関数名        Get-Temperature
  94:  # 概要          温度を取得する
  95:  # 引数          なし
  96:  # 戻り値        温度(取得できなかった場合は-1を返す)
  97:  # 戻り値型      Double
  98:  #############################################################
  99:  function Get-Temperature()
 100:  {
 101:      $retFault = -1
 102:      
 103:      # 温度計のデバイス名を取得する
 104:      $device = $FindUSB_PS.Invoke($null, @(0))
 105:   
 106:      # デバイス名が空文字の場合は -1 を返す。
 107:      if ($device -eq ''){return $retFault}
 108:   
 109:      # 温度/湿度を取得する
 110:      $ret = $GetTempHumidTrue_PS.Invoke($null, @($device))
 111:      
 112:      # 取得に失敗した場合は -1 を返す。
 113:      if ($ret -ne 0){return $retFault}
 114:   
 115:      # 取得に成功した場合の処理
 116:      $temp  = $USBMeter.GetField("g_temp").GetValue($null)
 117:      return $temp
 118:  }
 119:   
 120:   
 121:  #############################################################
 122:  # 関数名        Send-Mail
 123:  # 概要          メールを送信する
 124:  # 引数          以下のスイッチの中から1つ指定する
 125:  #                 -Normal   正常メール
 126:  #                 -Alarm    警報メール
 127:  #                 -Fault    失敗メール
 128:  # 戻り値        なし
 129:  # 戻り値型      なし
 130:  #############################################################
 131:  function Send-Mail($tempValue, [Switch]$Normal, [Switch]$Alarm, [Switch]$Fault)
 132:  {
 133:      if ($Normal.isPresent)
 134:      {
 135:          $mailConfig = $xml.USBMeter.NormalMail
 136:      }
 137:      elseif ($Alarm.isPresent)
 138:      {
 139:          $mailConfig = $xml.USBMeter.AlarmMail
 140:      }
 141:      elseif ($Fault.isPresent)
 142:      {
 143:          $mailConfig = $xml.USBMeter.FaultMail
 144:      }
 145:   
 146:      ## メール設定(共通)
 147:      $common = $xml.USBMeter.Common
 148:      $from   = $common.Mail.From
 149:      $smtp   = $common.Mail.Smtp
 150:      ## メール設定(個別)
 151:      # 送信アドレス
 152:      $OFS    = ','
 153:      $to     = [String]$mailConfig.Address
 154:      $OFS    = ' '
 155:      # サブジェクト
 156:      $subject   = $mailConfig.Subject
 157:      $tempStr   = "{0,4:##0.0}℃" -F $tempValue
 158:      $subject   = $subject.Replace('[TempValue]', $tempStr)
 159:      $date      = Get-Date -Uformat "%Y/%m/%d"
 160:      $subject   = $subject.Replace('[Date]', $date)
 161:      $time      = Get-Date -Uformat "%H:%M:%S"
 162:      $subject   = $subject.Replace('[Time]', $time)
 163:   
 164:      # メール送信
 165:      $mailer = New-Object System.Net.Mail.SmtpClient($smtp)
 166:      $mailer.Send($from, $to, $subject, "")
 167:      
 168:      # ログ出力
 169:      Write-Log $subject
 170:   
 171:      # CSV出力
 172:      Write-Csv $tempValue
 173:  }
 174:   
 175:  #############################################################
 176:  # 関数名        Write-Log
 177:  # 概要          ログファイルに文字列を書き込む
 178:  # 引数          書き込む文字列
 179:  # 戻り値        なし
 180:  # 戻り値型      なし
 181:  #############################################################
 182:  function Write-Log($msg)
 183:  {
 184:      $logMonth = Get-Date -Uformat "%Y%m"
 185:      $logDate  = Get-Date -Uformat "%Y%m%d"
 186:      $now     = Get-Date -Uformat "[%Y/%m/%d %H:%M:%S]"
 187:      $logfilePath = $logfilePathDef.Replace('[LogMonth]', $logMonth)
 188:      $logfilePath = $logfilePath.Replace('[LogDate]', $logDate)
 189:   
 190:      $logfileDir = Split-Path $logfilePath -Parent
 191:      if ((Test-Path $logfileDir) -eq $false){[void](md $logfileDir)}
 192:      $msg = "$now " + $msg
 193:      Write-Host $msg
 194:      $msg >> $logfilePath
 195:  }
 196:   
 197:  #############################################################
 198:  # 関数名        Write-Csv
 199:  # 概要          CSVファイルに時刻と温度データを書き込む
 200:  # 引数          温度
 201:  # 戻り値        なし
 202:  # 戻り値型      なし
 203:  #############################################################
 204:  function Write-Csv($temp)
 205:  {
 206:      $csvfileDir = Split-Path $csvfilePath -Parent
 207:      if ((Test-Path $csvfileDir) -eq $false){[void](md $csvfileDir)}
 208:   
 209:      $date = Get-Date -Uformat "%Y/%m/%d %H:%M:%S"
 210:      $data = [String]$date + "," + [String]$temp
 211:      $data | Out-File $csvfilePath -Append -Encoding Default
 212:  }
 213:   
 214:  ######################################################################
 215:  # メイン
 216:  ######################################################################
 217:   
 218:  # 各種設定を読み込む
 219:  if ($Test.isPresent)
 220:  {
 221:      # テストモードの場合
 222:      $xml = [xml](Get-Content $CONFIG_PATH_TEST)
 223:  }
 224:  else
 225:  {
 226:      # 通常モードの場合
 227:      $xml = [xml](Get-Content $CONFIG_PATH)
 228:  }
 229:   
 230:  # 温度の閾値
 231:  $tempThreshold = $xml.USBMeter.Temperature.Threshold
 232:  # チェック間隔(秒)
 233:  [int]$checkIntervalSec = $xml.USBMeter.CheckInterval.Second
 234:  # チェック間隔(分)
 235:  [String]$checkIntervalMin = "{0,4:0.00}" -F ($checkIntervalSec / 60)
 236:   
 237:  # ログファイルの相対パス
 238:  $logfilePathDef = Join-Path $scriptDir $xml.USBMeter.Common.Logfile
 239:  # CSVファイルの相対パス
 240:  $csvfilePath = Join-Path $scriptDir $xml.USBMeter.Common.Csvfile
 241:   
 242:  Write-Log '========================================='
 243:  if ($Test.isPresent)
 244:  {
 245:      # テストモードの場合
 246:      Write-Log '◆◆◆テストモードで起動しました◆◆◆'
 247:  }
 248:  else
 249:  {
 250:      # 通常モードの場合
 251:      Write-Log '◆◆◆通常モードで起動しました◆◆◆'
 252:  }
 253:  Write-Log '========================================='
 254:  Write-Log '   ■温度チェック:開始■'
 255:  Write-Log "    閾値:$tempThreshold ℃"
 256:  Write-Log "    間隔:$checkIntervalSec 秒($checkIntervalMin 分)"
 257:  Write-Log '========================================='
 258:   
 259:  # 指定されたチェック間隔で、温度のチェックを行う
 260:  while($true)
 261:  {
 262:      # 温度を取得する
 263:      $temperature = Get-Temperature
 264:      # 温度の取得に失敗した場合は「失敗メールを送る」
 265:      if($temperature -eq $CHECK_FAULT)
 266:      {
 267:          $temperature = -1
 268:          Send-Mail $temperature -Fault
 269:      }
 270:      # 温度が閾値を超えているかチェックする
 271:      elseif ($temperature -gt $tempThreshold)
 272:      {
 273:          # 閾値を超えている場合は「警報メールを送る」
 274:          Send-Mail $temperature -Alarm
 275:      }
 276:      else
 277:      {
 278:          # 閾値を超えていない場合は「正常メールを送る」
 279:          Send-Mail $temperature -Normal
 280:      }
 281:      Start-Sleep -Seconds $checkIntervalSec
 282:  }

定義ファイル(./config/config.xml

   1:  <?xml version="1.0" encoding="Shift_JIS"?>
   2:  <USBMeter>
   3:      <Temperature>
   4:          <Threshold>35</Threshold> 
   5:      </Temperature>
   6:   
   7:      <CheckInterval>
   8:          <Second>900</Second> 
   9:      </CheckInterval>
  10:   
  11:      <Common>
  12:          <Mail>
  13:              <From>'室温チェッカー'&lt;admin@xxx.com&gt;</From>
  14:              <Smtp>xxx.smtp.com</Smtp>
  15:          </Mail>
  16:          <Logfile>log/[LogMonth]/Temperature_[LogDate].log</Logfile>
  17:          <CsvFile>csv/Temperature.csv</CsvFile>
  18:      </Common>
  19:   
  20:      <NormalMail>
  21:          <Subject>[温度]OK : [TempValue] : [Date] [Time]</Subject>
  22:          <Address>admin@xxx.com</Address> 
  23:          <Address>yoshioka@xxx.com</Address> 
  24:      </NormalMail>
  25:   
  26:      <AlarmMail>
  27:          <Subject>[温度]NG : [TempValue] : [Date] [Time]</Subject>
  28:          <Address>admin@xxx.com</Address> 
  29:          <Address>yoshioka@xxx.com</Address> 
  30:      </AlarmMail>
  31:   
  32:      <FaultMail>
  33:          <Subject>[温度]NG : 検出失敗 : [Date] [Time]</Subject>
  34:          <Address>admin@xxx.com</Address> 
  35:          <Address>yoshioka@xxx.com</Address> 
  36:      </FaultMail>
  37:  </USBMeter>

2010-09-10

[]久々に書き込み

お久しぶりです。

業務に没頭していたらあっという間に2年経ってしまいました。(^^;


業務でちょっとしたツールが欲しいときは、たいていPowerShellで作っています。

マイペースで紹介していこうと思います。

BASIC認証つきHTTPSページへのアクセス

わりと最近作ったものとしては、

BASIC認証つきHTTPSページにあるXMLに定期的にアクセスして、情報を取得するスクリプトですかね。


CA署名のない証明書のサイトへアクセスする際、処理に苦労しました。


Delegateを使わなければ実現できなかったので、

以下のページからDownloadできるNew-Delegate.ps1を利用させてもらいました。

http://www.microsoft.com/japan/windowsserver2008/countdown2008/powershell/default.mspx

2008-05-28

[][]オセロゲームを作ろう(7):盤面描画とカーソル移動(2)

盤面描画とカーソル移動

前回紹介した、盤面描画とカーソル移動のサンプルについて解説します。

盤面描画

135 : # オセロ盤の描画
136 : function Draw-Board()
137 : {
138 :     Write-Host "┌─┬─┬─┬─┬─┬─┬─┬─┐" -ForegroundColor $boardFgColor -BackgroundColor $boardBgColor
139 :     for($i=1; $i -le 7;$i++)
140 :     {
141 :         Write-Host "│ │ │ │ │ │ │ │ │" -ForegroundColor $boardFgColor -BackgroundColor $boardBgColor
142 :         Write-Host "├─┼─┼─┼─┼─┼─┼─┼─┤" -ForegroundColor $boardFgColor -BackgroundColor $boardBgColor
143 :     }
144 :     Write-Host "│ │ │ │ │ │ │ │ │" -ForegroundColor $boardFgColor -BackgroundColor $boardBgColor
145 :     Write-Host "└─┴─┴─┴─┴─┴─┴─┴─┘" -ForegroundColor $boardFgColor -BackgroundColor $boardBgColor
146 : }

Draw-Init関数で最初の3行のメッセージを、

そこからコールされるDraw-Board関数オセロ盤を描画します。

#139〜143行目はfor文を使っているので可読性が悪いですね。。。(^^;


結果はこんな感じ。

==================================
      Othello  by  PowerShell
==================================
┌─┬─┬─┬─┬─┬─┬─┬─┐
│ │ │ │ │ │ │ │ │
├─┼─┼─┼─┼─┼─┼─┼─┤
│ │ │ │ │ │ │ │ │
├─┼─┼─┼─┼─┼─┼─┼─┤
│ │ │ │ │ │ │ │ │
├─┼─┼─┼─┼─┼─┼─┼─┤
│ │ │ │ │ │ │ │ │
├─┼─┼─┼─┼─┼─┼─┼─┤
│ │ │ │ │ │ │ │ │
├─┼─┼─┼─┼─┼─┼─┼─┤
│ │ │ │ │ │ │ │ │
├─┼─┼─┼─┼─┼─┼─┼─┤
│ │ │ │ │ │ │ │ │
├─┼─┼─┼─┼─┼─┼─┼─┤
│ │ │ │ │ │ │ │ │
└─┴─┴─┴─┴─┴─┴─┴─┘


カーソル移動

152 : # 指定した位置にカーソルを移動する
153 : function Move-Cursor([int]$newX, [int]$newY)
154 : {
155 :     $coordinate = New-Object System.Management.Automation.Host.Coordinates $newX, $newY
156 :     $rui.CursorPosition = $coordinate
157 : }

Move-Cursor関数引数に指定した座標にカーソルを移動します。

引数は移動先のX座標、Y座標です。

座標を指定してCoordinatesオブジェクトを生成し、CursorPositionに代入すればOKです。


ちなみに、オセロ盤の左上のセルの座標は、X=2、Y=4(上から5行目、左から3byte目)です。

上下左右キー操作

 46 :         # 上下左右のキー操作
 47 :         if($keyCode -eq $keyMap::Up)   {Move-CursorByDirection ([String]$keyMap::Up);   continue}
 48 :         if($keyCode -eq $keyMap::Down) {Move-CursorByDirection ([String]$keyMap::Down); continue}
 49 :         if($keyCode -eq $keyMap::Left) {Move-CursorByDirection ([String]$keyMap::Left); continue}
 50 :         if($keyCode -eq $keyMap::Right){Move-CursorByDirection ([String]$keyMap::Right);continue}

キー入力の監視で解説した、Get-InputKey関数の中で上下左右キーを捕捉します。

上下左右キーのカーソル移動は処理が似ているので、Move-CursorByDirection関数に集約させており、

その際、引数として、Stringにキャストした$KeyMap(Keys列挙体)を渡しています。

159 : # 上下左右のキーを押した時のカーソル移動
160 : function Move-CursorByDirection([String]$key)
161 : {
162 :     ($oldX, $oldY) = ($rui.CursorPosition.X, $rui.CursorPosition.Y)
163 :     if($key -eq "Up")       {$newX = $oldX; if($oldY -gt 0){$newY = $oldY - 1}}
164 :     elseif($key -eq "Down") {$newX = $oldX; $newY = $oldY + 1}
165 :     elseif($key -eq "Left") {if($oldX -gt 0){$newX = $oldX - 1}; $newY = $oldY}
166 :     elseif($key -eq "Right"){$newX = $oldX + 1; $newY = $oldY}
167 :     Move-Cursor $newX $newY
168 : }

Move-CursorByDirection関数では、まず162行目で現在のカーソル位置を取得。

163〜166行目で引数(Up,Down,Left,Rightの4種類)に応じて、カーソルの移動先を計算しています。

[][]オセロゲームを作ろう(8):盤面描画とカーソル移動(3)


盤面描画とカーソル移動

引き続き、盤面描画とカーソル移動のサンプルについての解説です。


アプリでは、エラーやデバッグ情報など、オセロ盤の下方にメッセージを表示します。

メッセージを表示する際、一時的にカーソルをオセロ盤下方に移動しますが、その後、カーソルを元の位置に復帰しなければなりません。


そこで必要になる処理が、カーソル位置のバックアップリストアです。

それぞれ、Push-Cursor関数、Pop-Cursor関数が担当します。

カーソル位置のバックアップ

170 : function Push-Cursor()
171 : {
172 :     $script:savedCursorPositionX = $rui.CursorPosition.X
173 :     $script:savedCursorPositionY = $rui.CursorPosition.Y
174 : }

カーソル位置のリストア

176 : function Pop-Cursor()
177 : {
178 :     Move-Cursor $script:savedCursorPositionX $script:savedCursorPositionY
179 : }

2008-05-27

[][]オセロゲームを作ろう(6):盤面描画とカーソル移動(1)

盤面描画とカーソル移動

今回はオセロの盤面描画とカーソル移動です。

以下のサンプルを実行してみてください。

  1 : ###############################################################################
  2 : # 初期処理/定義
  3 : ###############################################################################
  4 : # アセンブリ読み込み/参照
  5 : $rui = $host.UI.RawUI
  6 : [void][Reflection.Assembly]::LoadWithPartialName("System.Windows.Forms") 
  7 : $keyMap = [Windows.Forms.Keys]
  8 : # 初期化/定義
  9 : $boardStartLine = 3
 10 : ($startX, $startY) = (2, ($boardStartLine + 1))
 11 : ($msgStartLine, $debugMsgStartLine)     = (21, 23)
 12 : $script:screen = $null
 13 : #色定義
 14 : ($boardFgColor, $boardBgColor) = ("Black", "White")
 15 : 
 16 : ###############################################################################
 17 : # メイン
 18 : ###############################################################################
 19 : function Start-Game()
 20 : {
 21 :     Backup-ScrBuf
 22 :     Draw-Init
 23 :     Get-InputKey
 24 :     Restore-ScrBuf
 25 : }
 26 : 
 27 : ###############################################################################
 28 : # キー入力受付関連の関数
 29 : ###############################################################################
 30 : 
 31 : # キー入力関数
 32 : function Get-InputKey()
 33 : {
 34 :     while($true)
 35 :     {
 36 :         # キー取得
 37 :         $keyInfo = $rui.ReadKey("NoEcho,IncludeKeyDown")
 38 :         $keyCode = $keyInfo.VirtualKeyCode
 39 : 
 40 :         # Enterを入力
 41 :         if($keyCode -eq $keyMap::Enter){continue}
 42 : 
 43 :         # Escを入力→終了
 44 :         if($keyCode -eq $keyMap::Escape){return}
 45 : 
 46 :         # 上下左右のキー操作
 47 :         if($keyCode -eq $keyMap::Up)   {Move-CursorByDirection ([String]$keyMap::Up);   continue}
 48 :         if($keyCode -eq $keyMap::Down) {Move-CursorByDirection ([String]$keyMap::Down); continue}
 49 :         if($keyCode -eq $keyMap::Left) {Move-CursorByDirection ([String]$keyMap::Left); continue}
 50 :         if($keyCode -eq $keyMap::Right){Move-CursorByDirection ([String]$keyMap::Right);continue}
 51 : 
 52 :         # Aを入力→現在の座標を表示
 53 :         if($keyCode -eq $keyMap::A)
 54 :         {
 55 :             $dX = $rui.CursorPosition.X
 56 :             $dY = $rui.CursorPosition.Y
 57 :             $dMsg = "X : " + $dX + " Y : " + $dY
 58 :             Show-DebugMsg $dMsg
 59 :         }
 60 : 
 61 :     }
 62 : }
 63 : 
 64 : ###############################################################################
 65 : # メッセージ表示関数
 66 : ###############################################################################
 67 : 
 68 : # メッセージ表示
 69 : function Show-Msg($msg)
 70 : {
 71 :     Show-MsgInner $msgStartLine $msg $msgFgColor $msgBgColor
 72 : }
 73 : 
 74 : function Show-DebugMsg($msg)
 75 : {
 76 :     Show-MsgInner $debugMsgStartLine $msg $boardFgColor $boardBgColor
 77 : }
 78 : 
 79 : function Show-MsgInner([int]$pos, [String]$msg, [ConsoleColor]$fg, [ConsoleColor]$bg)
 80 : {
 81 :     Push-Cursor
 82 :     Move-Cursor 0 $pos
 83 :     Write-Host -NoNewLine "                                                                                    "
 84 :     Move-Cursor 0 $pos
 85 :     Write-Host -NoNewLine $msg -ForegroundColor $fg -BackgroundColor $bg
 86 :     Pop-Cursor
 87 : }
 88 : 
 89 : ###############################################################################
 90 : # スクリーン操作関数
 91 : ###############################################################################
 92 : 
 93 : # スクリーンバッファバックアップ
 94 : function Backup-ScrBuf()
 95 : {
 96 :     $rect = New-Object System.Management.Automation.Host.Rectangle
 97 :     $rect.Left   = 0
 98 :     $rect.Top    = 0
 99 :     $rect.Right  = $rui.WindowSize.Width
100 :     $rect.Bottom = $rui.CursorPosition.Y
101 :     $script:screen = $rui.GetBufferContents($rect)
102 : }
103 : 
104 : # スクリーンバッファリストア
105 : function Restore-ScrBuf()
106 : {
107 :     Clear-Host
108 :     $origin = New-Object System.Management.Automation.Host.Coordinates(0, 0)
109 :     $rui.SetBufferContents($origin, $script:screen)
110 :     $pos = New-Object System.Management.Automation.Host.Coordinates(0, $script:screen.GetUpperBound(0))
111 :     $rui.CursorPosition = $pos
112 : }
113 : 
114 : ###############################################################################
115 : # 描画関連の関数
116 : ###############################################################################
117 : 
118 : # 初期描画
119 : function Draw-Init
120 : {
121 :     # タイトルの描画
122 :     Clear-Host
123 :     Write-Host "=================================="
124 :     Write-Host "      Othello  by  PowerShell     "
125 :     Write-Host "=================================="
126 : 
127 :     # オセロ盤の描画
128 :     Move-Cursor 0 $boardStartLine
129 :     Draw-Board
130 : 
131 :     # カーソル位置の初期化
132 :     Move-Cursor $startX $startY
133 : }
134 : 
135 : # オセロ盤の描画
136 : function Draw-Board()
137 : {
138 :     Write-Host "┌─┬─┬─┬─┬─┬─┬─┬─┐" -ForegroundColor $boardFgColor -BackgroundColor $boardBgColor
139 :     for($i=1; $i -le 7;$i++)
140 :     {
141 :         Write-Host "│ │ │ │ │ │ │ │ │" -ForegroundColor $boardFgColor -BackgroundColor $boardBgColor
142 :         Write-Host "├─┼─┼─┼─┼─┼─┼─┼─┤" -ForegroundColor $boardFgColor -BackgroundColor $boardBgColor
143 :     }
144 :     Write-Host "│ │ │ │ │ │ │ │ │" -ForegroundColor $boardFgColor -BackgroundColor $boardBgColor
145 :     Write-Host "└─┴─┴─┴─┴─┴─┴─┴─┘" -ForegroundColor $boardFgColor -BackgroundColor $boardBgColor
146 : }
147 : 
148 : ###############################################################################
149 : # カーソル操作関数
150 : ###############################################################################
151 : 
152 : # 指定した位置にカーソルを移動する
153 : function Move-Cursor([int]$newX, [int]$newY)
154 : {
155 :     $coordinate = New-Object System.Management.Automation.Host.Coordinates $newX, $newY
156 :     $rui.CursorPosition = $coordinate
157 : }
158 : 
159 : # 上下左右のキーを押した時のカーソル移動
160 : function Move-CursorByDirection([String]$key)
161 : {
162 :     ($oldX, $oldY) = ($rui.CursorPosition.X, $rui.CursorPosition.Y)
163 :     if($key -eq "Up")       {$newX = $oldX; if($oldY -gt 0){$newY = $oldY - 1}}
164 :     elseif($key -eq "Down") {$newX = $oldX; $newY = $oldY + 1}
165 :     elseif($key -eq "Left") {if($oldX -gt 0){$newX = $oldX - 1}; $newY = $oldY}
166 :     elseif($key -eq "Right"){$newX = $oldX + 1; $newY = $oldY}
167 :     Move-Cursor $newX $newY
168 : }
169 : 
170 : function Push-Cursor()
171 : {
172 :     $script:savedCursorPositionX = $rui.CursorPosition.X
173 :     $script:savedCursorPositionY = $rui.CursorPosition.Y
174 : }
175 : 
176 : function Pop-Cursor()
177 : {
178 :     Move-Cursor $script:savedCursorPositionX $script:savedCursorPositionY
179 : }
180 : 
181 : # 処理スタート
182 : Start-Game

上記コードを実行すると、オセロの盤面を描画します。

実行直後

f:id:newpops:20080527014052j:image


上下左右キーを押下すると、キーの方向にカーソルが「1」移動します。

また、Aキーを押下すると現在のカーソル位置を表示します。

#カーソル位置を表示する処理はデバッグ目的で実装しています。

下に移動+カーソル位置表示

起動後、下キーを押下し、下方向に1移動。

その後、Aキーを押下し、カーソル位置を表示しました。

f:id:newpops:20080527014053j:image



ただ、このままではオセロ盤の枠上にもカーソルが移動してしまいます。

このオセロゲームを、

  • 上下左右キーでカーソル移動
  • Enterで駒を置く

という仕様にする場合、カーソルが枠上に移動可能なのは好ましくありません。

駒が配置可能な8×8のマス上にカーソル移動を制限する必要があります。


つづく。。。

2008-05-15

[][]オセロゲームを作ろう(5):スクリーンバッファ操作(3)

スクリーンバッファの操作

前回に引き続き、前々回紹介した、スクリーンバッファ操作のサンプルについて解説します。

スクリーンバッファリストア

# スクリーンバッファリストア
function Restore-ScrBuf()
{
    Clear-Host
    $origin = New-Object System.Management.Automation.Host.Coordinates(0, 0)
    $rui.SetBufferContents($origin, $script:screen)
    $pos = New-Object System.Management.Automation.Host.Coordinates(0, $script:screen.GetUpperBound(0))
    $rui.CursorPosition = $pos
}

Restore-ScrBuf関数は、Backup-ScrBuf関数バックアップしたスクリーンバッファリストアします。

手順は以下です。

  1. コンソールのクリア(Clear-Host)
  2. スクリーンバッファの描画座標の生成(Coordinatesオブジェクト
  3. スクリーンバッファの描画(SetBufferContentsメソッド)
  4. カーソル位置を描画領域の最終行に移動(CursorPositionプロパティ

まず、Clear-Host関数でコンソールをクリアします。

描画座標はコンソール左上ですので、X=0,Y=0のCoordinatesオブジェクトを生成します。

次に、SetBufferContentsメソッドでスクリーンバッファを描画します。

SetBufferContentsメソッド
System.Void SetBufferContents(Coordinates origin, BufferCell[,] contents)
System.Void SetBufferContents(Rectangle r, BufferCell fill)

SetBufferContentsメソッドは2種類ありますが、今回利用するのは前者で、

引数は、描画座標(Coordinatesオブジェクト)とスクリーンバッファ(BufferCellオブジェクトの2次元配列)です。


最後に、カーソル位置を描画領域の最終行に移動します。

最終行は、リストアするスクリーンバッファの1次元目の最終配列要素(最終行)になります。


つづく。。。