Itsukaraの日記

最新IT技術を勉強・実践中。最近はDeep Learningに注力。

PowerShellでGUI: 電卓を作ってみました

これまで色々な言語や環境を勉強した際に、練習台として電卓を試作したので、PowerShellWPFでも電卓を作ってみました。なお、下記は別の言語/環境での電卓の記事です。合わせてお読みいただければ幸いです。

他の言語との比較

GUI表現であるWPF以外は、Javascript版をかなり流用できました。
Javascriptとの違いは下記程度です。
・関数内にスコープが閉じた変数は、変数名の先頭に「$」を付加
・値が設定されるグローバル変数は、変数名の先頭に「$global:」を付加。
・比較演算子の書換( "=="→"-eq"、"!="→"-ne" 等)。
・ライブラリ呼出しの書換("Math.abs"→"[Math]::Abs"等)。
・switch構文をPowershellの構文に書換(ソースコード参照)。

ソースコード

#■プログラムの説明
#・WPFを使った電卓です。
#■実行方法
#・本ファイルがあるフォルダでコマンドプロンプトを開き下記を実行
# powershell -sta -file WpfPocketCalc.ps1
#■画面基本操作
#・非常に基本的な電卓と同じ操作方法です。
#・ただし、計算結果は全て整数です。
#・また、BSは最後に入力した数字を1文字削除します。
#■バージョン等
#・プログラム名:WpfPocketCalc.ps1
#・バージョン :V1.0.0
#・作成日   :2017/04/05
#・最終更新日 :2017/04/05
#・作成者   :Itsukara (Takayoshi Iitsuka)、iitt21-t@yahoo.co.jp、http://itsukara.hateblo.jp

$ErrorActionPreference = "stop"
Set-PSDebug -Strict

try { Add-Type -AssemblyName PresentationCore,PresentationFramework,WindowsBase,System.Windows.Forms } 
catch { throw "Failed to load Windows Presentation Framework assemblies." }

# For each name in <xxx x:Name="name">, create new variable and set value  
Function setVaviables ($xaml, $form) {
    $xaml.SelectNodes("//*") | ? {$_.Attributes["x:Name"] -ne $null} | % {
        New-Variable  -Name $_.Name -Value $form.FindName($_.Name) -Force -Scope Global
    }
}

# xaml設定
[xml]$xaml = @' 
<Window 
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="WPF Pocket Calc presented by Itsukara" Height="300" Width="300">
    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="1*"/>
            <ColumnDefinition Width="1*"/>
            <ColumnDefinition Width="1*"/>
            <ColumnDefinition Width="1*"/>
        </Grid.ColumnDefinitions>
        <Grid.RowDefinitions>
            <RowDefinition Height="40"/>
            <RowDefinition Height="1*"/>
            <RowDefinition Height="1*"/>
            <RowDefinition Height="1*"/>
            <RowDefinition Height="1*"/>
            <RowDefinition Height="1*"/>
        </Grid.RowDefinitions>
        <TextBox x:Name="displayLine" Grid.ColumnSpan="4" TextWrapping="Wrap" Text="0" FontSize="26" TextAlignment="Right" Margin="10,0"/>
        <Button x:Name="buttonBS" Content="BS" Grid.Column="0"  Grid.Row="1" FontSize="30"/>
        <Button x:Name="buttonC" Content="C" Grid.Column="1"  Grid.Row="1" FontSize="30"/>
        <Button x:Name="buttonAC" Content="AC" Grid.Column="2"  Grid.Row="1" FontSize="30"/>
        <Button x:Name="buttonAdd" Content="+" Grid.Column="3"  Grid.Row="1" FontSize="30" FontWeight="Bold"/>
        <Button x:Name="button7" Content="7" Grid.Column="0"  Grid.Row="2" FontSize="30"/>
        <Button x:Name="button8" Content="8" Grid.Column="1"  Grid.Row="2" FontSize="30"/>
        <Button x:Name="button9" Content="9" Grid.Column="2"  Grid.Row="2" FontSize="30"/>
        <Button x:Name="buttonSub" Content="-" Grid.Column="3"  Grid.Row="2" FontSize="30" FontWeight="Bold"/>
        <Button x:Name="button4" Content="4" Grid.Column="0"  Grid.Row="3" FontSize="30"/>
        <Button x:Name="button5" Content="5" Grid.Column="1"  Grid.Row="3" FontSize="30"/>
        <Button x:Name="button6" Content="6" Grid.Column="2"  Grid.Row="3" FontSize="30"/>
        <Button x:Name="buttonMul" Content="×" Grid.Column="3"  Grid.Row="3" FontSize="30" FontWeight="Bold"/>
        <Button x:Name="button1" Content="1" Grid.Column="0"  Grid.Row="4" FontSize="30"/>
        <Button x:Name="button2" Content="2" Grid.Column="1"  Grid.Row="4" FontSize="30"/>
        <Button x:Name="button3" Content="3" Grid.Column="2"  Grid.Row="4" FontSize="30"/>
        <Button x:Name="buttonDiv" Content="/" Grid.Column="3"  Grid.Row="4" FontSize="20" FontWeight="Bold"/>
        <Button x:Name="button0" Content="0" Grid.Column="0"  Grid.Row="5" FontSize="30"/>
        <Button x:Name="buttonEql" Content="=" Grid.Column="1"  Grid.Row="5" FontSize="30" FontWeight="Bold"/>
        <Button x:Name="buttonSgn" Content="±" Grid.Column="2"  Grid.Row="5" FontSize="30" FontWeight="Bold"/>
        <Button x:Name="buttonMod" Content="%" Grid.Column="3"  Grid.Row="5" FontSize="30" FontWeight="Bold"/>
    </Grid>
</Window>
'@

# 変数初期化
$maxKeta = 15;
$maxDouble = 999999999999999.0;
$OVERFLOW  = "OVERFLOW (Push AC)";
$ZERODIV   = "DIV BY 0 (Push AC)";
$operand1  = "";
$operator  = "";
$firstNum  = $true;


# form作成
$form       = [Windows.Markup.XamlReader]::Load((New-Object System.Xml.XmlNodeReader $xaml))

# $displayLineの値として、form中のdisplayLine Nodeを設定
$displayLine = $form.FindName("displayLine")

# callback設定

## Function for button action
Function button_action($buttonStr) {
	$display = $displayLine.Text
	if ($display -eq $OVERFLOW -or $display -eq $ZERODIV) {
	    if ($buttonStr -ne "AC") {
	        return
	    }
	}

	switch -regex ($buttonStr) {
	    "0|1|2|3|4|5|6|7|8|9" {
	        if ($global:firstNum) {
	            if ($global:operator -ne "") {
	                $global:operand1 = $display
	            }
	            $display = "0"
	            $global:firstNum = $false
	        }
	        if ($display.Length -ge $maxKeta) {
	            $displayLine.Text = $OVERFLOW
	        } else {
		        $displayNum = +$display
		        $buttonNum = +$buttonStr
		        $displayNum = $displayNum * 10 + $buttonNum
		        $displayLine.Text = $displayNum
	        }
	    }
	    
	    "+|-|×|/|=|%" {
	        if ($global:operator -ne "" -and $global:operand1 -ne "" -and !$global:firstNum) {
	            $operand1Num = +$global:operand1
	            $operand2Num = +$display
	            switch ($global:operator) {
	                "+" {
	                    if ([Math]::Abs($operand1Num + $operand2Num) -gt $maxDouble) {
	                        $displayLine.Text = $OVERFLOW
	                    } else {
	                        $displayLine.Text = $operand1Num + $operand2Num
	                    }
	                }
	                
	                "-" {
	                    if ([Math]::Abs($operand1Num - $operand2Num) -gt $maxDouble) {
	                        $displayLine.Text = $OVERFLOW
	                    } else {
	                        $displayLine.Text = $operand1Num - $operand2Num
	                    }
	                }
	                
	                "×" {
	                    if ([Math]::Abs($operand1Num * $operand2Num) -gt $maxDouble) {
	                        $displayLine.Text = $OVERFLOW
	                    } else {
	                        $displayLine.Text = $operand1Num * $operand2Num
	                    }
	                }
	                
	                "/" {
	                    if ($operand2Num -eq 0) {
	                        $displayLine.Text = $ZERODIV
	                    } else {
	                        $displayLine.Text = [Math]::Floor($operand1Num / $operand2Num)
	                    }
	                }
	                
	                "%" {
	                    if ($operand2Num -eq 0) {
	                        $displayLine.Text = $ZERODIV
	                    } else {
	                        $displayLine.Text = $operand1Num % $operand2Num
	                    }
	                }
	            }
	        }
	        if ($buttonStr -eq "=") {
	            $global:operator = ""
	        } else {
	            $global:operator = $buttonStr
	        }
	        $global:firstNum = $true
	    }
	    
	    "BS" {
	        $displayNum = +$display
	        $displayNum = [Math]::Floor($displayNum / 10)
	        $displayLine.Text = $displayNum
	     }
	     
	    "±" {
	        $displayNum = +$display
	        $displayNum = -$displayNum
	        $displayLine.Text = $displayNum
	     }
	     
	    "C" {
	        $displayLine.Text = "0"
	     }
	     
	    "AC" {
	        $displayLine.Text = "0"
	        $global:operand1 = ""
	        $global:operator = ""
	     }
	}
}

## Function to add Click callback
Function button_add_Click($button) {
    $btn = $form.FindName($button.Name)
    if ($btn.add_Click -ne $null) {
        $btn.add_Click({button_action $this.content})
    }
}

## Add_Click to all button
$xaml.SelectNodes("//*") | ? {$_.Name -clike "button*"} | %  {button_add_Click $_}

# 全体
$result = $form.ShowDialog()

github

上記ソースコードは、Wpfzooのgithubに加えておきました。
github.com

追記

よく考えたら、Pythonでは電卓を作ってませんでした。

Pythonはグラフを描いたりするのには重宝しますが、GUIはどうなのだろう。

PowerShellでGUI:WPFの部品をほぼ全て含むデモ公開

PowerShellWPFGUIを表示できることが分かり、Visual Studioを使うとWPFGUIを簡単にプレビュー/設計できることが分かったので、WPFの全ての部品(コントロール)を試してみようと思い、デモプログラムを作成しました。下記がそのスクリーンショットです。

WpfZoo.ps (WPF部品の動物園)

  • WPFの色々なコントロールを表示するデモプログラムです。
  • 表示だけでなくクリックしたボタンの表示(画面下部に表示)等も行います。
  • なお、下記はスクリーンショットなのでクリックできません、プログラムをgithub(最後の方に記載)からダウンロードし、実行して試してください。
TabItem1 (最初の画面、色々な部品を配置)
  • ボタン、リスト、テーブル等をクリックしてみてください。

f:id:Itsukara:20170402211150p:plain

TabItem2 (色々な部品を配置)
  • カレンダー、Expanderや、スライダーを試してみてください。

f:id:Itsukara:20170402211402p:plain

Tab for DocPanel (DocPanelのデモ)
  • MenuItemや、画面左のTreeViewで項目を選択してみてください。

f:id:Itsukara:20170402211413p:plain

Tab for DocumentViewer (FrDocumentViewerのデモ)
  • xpsのビューアーです、ズーム等ができます。

f:id:Itsukara:20170402211418p:plain

Frames (Frameのデモ)
  • 画面上のボタンをクリックしてFrameを表示してみてください。

f:id:Itsukara:20170402211505p:plain

Media (MediaElementのデモ)
  • 動画の再生、一時停止、再生ができます。

f:id:Itsukara:20170402211518p:plain

RichTextBox (RichTextBoxのデモ)
  • 表示内容の書換、文字属性や文節属性の変更ができます。
    • ただし「下付き」「上付き」は、数字以外は上手く行きません。(原因不明)

f:id:Itsukara:20170404011522p:plain

画面上の各WPF部品に対応するxamlの確認方法
  • Visual StudioWPFプロジェクトを新規作成(ここここを参照)
  • Visual Studioの画面下部に表示されたxaml文字列の<Grid>と</Grid>で囲まれた部分を、プログラム内のxamlの<Grid>と</Grid>で囲まれた部分で置換
    • 事前にxaml中の文字列PSScriptRootを、プログラムを置いたパスで置換
    • これにより、プログラムと同様の画面がVisual Studioで表示される
  • Visual Studioの画面上のWPF部品をクリックして選択すると、画面下部のxaml中で対応する行が選択されるので、WPF部品とxaml文字列の対応が分かる

WpfFrame*.ps (Frameのちゃんとしたデモ)

上記のうち、Frameは、単に表示するだけで、表示したFrameでのアクションは何もないものでした。これでは使えないので、Frameのデモも作成しました。下記がその画面です。あまり複雑な画面に見えませんが、分からないことが多く、結構苦労しました。

最初の画面
  • 表示された画面左上の「スタート」をクリック

f:id:Itsukara:20170402211800p:plain

Frame内にPage1を表示
  • 名前等を入力後、「次へ」をクリック

f:id:Itsukara:20170402211805p:plain

Frame内にPage2を表示
  • 好きなフルーツを選択後、「次へ」をクリック

f:id:Itsukara:20170402211809p:plain

Frame内にPage3を表示
  • 「上記内容を確認しました」をチェックし、「登録」をクリック
    • 未チェックではエラーが出て、チェックするまで登録できない

f:id:Itsukara:20170402211814p:plain

Frame内にPage4を表示
  • この後、更に「スタート」をクリックすると次の登録画面になる

f:id:Itsukara:20170402211819p:plain

  • 補足情報
    • 登録情報の履歴が、画面下に表示される(この欄はスクロール可能)
    • 画面左上の「←」と「→」によって、画面を戻ったり、進めたりできる
    • この際に、既に入力した情報は保存される
苦労話

Frameでは、元々、外部フィルにPageのxamlファイルを置いて、Frame.Navigate()で読み込むしかないと思っていたので、callbackをいつ設定したら良いかわからず、実現に苦労しました。

ところが、よくよく見ると、Frame.Navigate()の引数としては、xamlでなく、ロード済みのformを渡すこともできることが分かりました。よって、あらかじめxamlを基にformをLoadして、callbackを設定すればよいので、とてもスッキリした作りになりました。

ソースコード

ソースコードは長いのでblogに記載せず、githubにアップロードしました。プログラムの説明も、ソースコードに少し書いていますので、読んでお試しください。

github.com

あとがき

参考になる情報もあり、WpfZooは結構簡単に作れたのですが、Frameは適切な例がなく、かなり苦労しました。しかし、苦労の過程で、Visual StudioでのWPF画面の確認方法や設計のコツ、PowerShell ISEでのデバッグ方法なども習得することができました。

PowerShellでGUI: 部品配置はGridとテキスト編集が良い

2つ前の記事「PowerShellでGUI: 可変個数RadioButton実現方法(別法) - Itsukaraの日記」で、PowerShellGUIVisual Studioで作成する方法を書きました。ただ、GUIを実現するWPFの勉強が不足していたので、GUI画面上にGUI部品を適当に貼り付けていました。そのため、部品の位置等を表す数字「Margin="68,119,0,0"」が沢山あり、後で調整するのが大変そうと感じました。

f:id:Itsukara:20170326015246p:plain

その後、WPFを少し調べたところ、GRIDを使うことにより部品をテーブル状に整然と並べることが出来ると分かりました。下記のような感じです。
f:id:Itsukara:20170326020148p:plain

ただ、GRIDへのGUI部品配置をGUIでやると、微調整が結構面倒なので、1~2個部品を並べたら、あとは、XAMLテキストをコピー・ペーストし適切な修正を行うのが良いです。

また、GUIで部品を配置すると、HeightやMarginなどに、色々な数字が入りますが、これらが部品ごとに入っていると、後で纏めて変えるのが面倒なので、それらの数字はできるだけ削っておいた方が良いです。順序としては、上記のコピー・ペーストの前にやっておく必要がありますね。

なお、Syleを使うと、後でTextBoxのHeightを纏めて30にしたりといったことが、簡単にできます。Styleは、<Window.Resources>というタグ内に記述します。

結果として、次のようなプログラムになりました。表示内容は、依然とあまり変わりませんが、無駄な数字がなくなって、すっきりしています。その分、後で変更するのが楽(なはず)です。

  • プログラム例-
$ErrorActionPreference = "stop"
Set-PSDebug -Strict
Add-Type -AssemblyName PresentationFramework

[xml]$xaml = @'
<Window
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="350" Width="525">
    <Window.Resources>
        <Style TargetType="TextBox">
            <Setter Property="Height" Value="30"/>
        </Style>
        <Style TargetType="Button">
            <Setter Property="Height" Value="30"/>
        </Style>
        <Style TargetType="TextBlock">
            <Setter Property="Foreground" Value="Blue"/>
        </Style>
    </Window.Resources>

    <Grid Margin="10,10,10,10">
        <Grid.RowDefinitions>
            <RowDefinition Height="30"/>
            <RowDefinition Height="30"/>
            <RowDefinition Height="30"/>
            <RowDefinition Height="30"/>
            <RowDefinition Height="30"/>
            <RowDefinition Height="10"/>
            <RowDefinition Height="30"/>
            <RowDefinition/>
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="50"/>
            <ColumnDefinition/>
        </Grid.ColumnDefinitions>

        <Label x:Name="label01" Content="名前" Margin="0" VerticalAlignment="Center" HorizontalAlignment="Right"/>
        <TextBox x:Name="textName" Margin="0" TextWrapping="Wrap" VerticalAlignment="Center" Grid.Column="1"/>

        <Label x:Name="label02" Content="ふりがな" Margin="0" VerticalAlignment="Center" HorizontalAlignment="Right" Grid.Row="1"/>
        <TextBox x:Name="textFurigana" Margin="0" TextWrapping="Wrap" VerticalAlignment="Center" Grid.Column="1" Grid.Row="1" />

        <Label x:Name="label03" Content="性別" Margin="0" VerticalAlignment="Top" HorizontalAlignment="Right" Grid.Row="2"/>
        <StackPanel x:Name="stackGender" HorizontalAlignment="Left" Margin="0" VerticalAlignment="Center" Orientation="Horizontal" Grid.Column="1" Grid.Row="2" Width="120">
            <RadioButton x:Name="radioButtonMale" Content="Male" VerticalAlignment="Center" HorizontalAlignment="Left" Margin="0,0,10,0"/>
            <RadioButton x:Name="radioButtonFemale" Content="Female" VerticalAlignment="Center" HorizontalAlignment="Left" Margin="0"/>
        </StackPanel>

        <Label x:Name="label04" Content="国籍" Margin="0" VerticalAlignment="Center" HorizontalAlignment="Right" Grid.Row="3"/>
        <ComboBox x:Name="comboBoxCountry" HorizontalAlignment="Left" Margin="0" VerticalAlignment="Center" Grid.Row="3" Grid.Column="1" Width="100"/>
        
        <Label x:Name="label05" Content="年齢" Margin="0" VerticalAlignment="Center" HorizontalAlignment="Right" Grid.Row="4"/>
        <TextBox x:Name="textAge" Margin="0" TextWrapping="Wrap" VerticalAlignment="Center" Grid.Column="1" Grid.Row="4" Width="50" HorizontalAlignment="Left" />

        <Button x:Name="buttonRegist" Content="登録" Margin="0" VerticalAlignment="Center" Grid.Column="0" Grid.ColumnSpan="2" Grid.Row="6" />
        <TextBlock x:Name="textBlock" HorizontalAlignment="Left" Margin="0" TextWrapping="Wrap" VerticalAlignment="Top" Grid.ColumnSpan="2" Grid.Row="7"/>

    </Grid>
</Window>
'@

$reader = New-Object System.Xml.XmlNodeReader $xaml
$frm = [System.Windows.Markup.XamlReader]::Load($reader)

$countries = "U.S.A China Japan Germany France England Russia".split(" ")
$comboBoxCountry = $frm.FindName("comboBoxCountry")
foreach ($country in $countries) {
  [void]$comboBoxCountry.Items.Add($country)
}

$textName = $frm.FindName("textName")
$comboBoxCountry = $frm.FindName("comboBoxCountry")
$textFurigana = $frm.FindName("textFurigana")
$stackGender = $frm.FindName("stackGender")
$textAge = $frm.FindName("textAge")
$textBlock = $frm.FindName("textBlock")

function getChecked($parent) {
    $checked = ""
    foreach ($child in $parent.Children) {
        if ($child.isChecked) {
            $checked += $child.Content
        }
    }
    return $checked
}

function regist {
    $name = $textName.Text
    $furigata = $textFurigana.Text
    $gender = getChecked $stackGender
    $country = $comboBoxCountry.SelectedValue
    $age = $textAge.Text

    $msg = ""
    $msg += "名前:" + $name + "`n" 
    $msg += "ふりがな:" + $furigata + "`n" 
    $msg += "性別:" + $gender + "`n" 
    $msg += "国籍:" + $country + "`n" 
    $msg += "年齢:" + $age + "`n" 

    $textBlock.Text = $msg
}

$buttonRegist =  $frm.FindName("buttonRegist")
$buttonRegist.Add_Click({regist})

$result = $frm.ShowDialog()
  • 画面表示例

f:id:Itsukara:20170326021108p:plain

PowerShellでGUI: 可変個数RadioButton実現方法(別法)

VisualStudioでPowerShell用GUIを作成する方法 - Itsukaraの日記」という記事で、PowerShellGUI作成用にVisualStudioを使う方法を紹介し、可変個数のRadioButtonを表示する方法も書きました。しかし、可変個数のRadioButtonは、もっと良い実現方法を考えたので、紹介します。

とても簡単で、GUIの構造を表すXAML文字列を書き換えるだけです。下記にプログラム例を示します。下記の「add_items_to_xaml」という関数が、XAMLを書き換える処理です。汎用性があるので、RadioButton以外でも使えると思います。

  • 可変個数のRadioButtonを作る例:
$ErrorActionPreference = "stop"
Set-PSDebug -Strict
Add-Type -AssemblyName PresentationFramework

$xaml = @'
<Window
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Title="xaml test #2"
    Width="170pt" Height="170pt">
  <StackPanel>
    <StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
      <StackPanel x:Name="country">
      </StackPanel>
      <Label Content="  " FontSize="10pt"/>
      <StackPanel x:Name="fruit">
      </StackPanel>
    </StackPanel>
    <Label x:Name="msg"  Content="  " FontSize="10pt"/>
    <Button x:Name="btn" Content="Push Me"  FontSize="10pt" />
  </StackPanel>
</Window>
'@

# 項目一覧を$xamlに追加
# $head : 置換前文字列
# $add  : 置換後文字列のひな型
# $placeholder: $add中のプレスホルダー文字列
# $items: 項目一覧(空白で区切られたワード列)
function add_items_to_xaml($head, $add, $placeholder, $items) {
    $list = $items.split(" ")
    [array]::Reverse($list)
    foreach ($item in $list) {
        $add_item = $add.Replace($placeholder, $item)
        $global:xaml = $global:xaml.Replace($head, $add_item)
    }
}

# countryへの項目追加
$country_head = @'
      <StackPanel x:Name="country">
'@
$country_add = @'
      <StackPanel x:Name="country">
        <RadioButton Content="PlaceHolder"></RadioButton>
'@
$countries = "U.S.A China Japan Germany France England Russia"
add_items_to_xaml $country_head $country_add "PlaceHolder" $countries

# fruitへの項目追加
$fruit_head = @'
      <StackPanel x:Name="fruit">
'@
$fruit_add = @'
      <StackPanel x:Name="fruit">
        <RadioButton Content="PlaceHolder"></RadioButton>
'@
$fruits = "Apple Orange Grape"
add_items_to_xaml $fruit_head $fruit_add "PlaceHolder" $fruits

$xaml

$xaml = [xml]$xaml
$reader = New-Object System.Xml.XmlNodeReader $xaml
$frm = [System.Windows.Markup.XamlReader]::Load($reader)
$country = $frm.FindName("country")
$fruit = $frm.FindName("fruit")
$msg = $frm.FindName("msg")

# 選択された項目を返す
function getChecked($parent) {
    $checked = ""
    foreach ($child in $parent.Children) {
        if ($child.isChecked) {
            $checked += $child.Content
        }
    }
    return $checked
}

# ボタンクリック時の処理
function clicked {
    $selectedCountry = getChecked $country
    $selectedfruit  = getChecked $fruit
    $msg.content = "Country=" + $selectedCountry + "`n" + "Fruit=" +  $selectedfruit
}

$btn = $frm.FindName("btn")
$btn.Add_Click({clicked})
$result = $frm.ShowDialog()
  • 書換後のXAML(上記プログラムで出力したもの)
<Window
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Title="xaml test #2"
    Width="170pt" Height="170pt">
  <StackPanel>
    <StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
      <StackPanel x:Name="country">
        <RadioButton Content="U.S.A"></RadioButton>
        <RadioButton Content="China"></RadioButton>
        <RadioButton Content="Japan"></RadioButton>
        <RadioButton Content="Germany"></RadioButton>
        <RadioButton Content="France"></RadioButton>
        <RadioButton Content="England"></RadioButton>
        <RadioButton Content="Russia"></RadioButton>
      </StackPanel>
      <Label Content="  " FontSize="10pt"/>
      <StackPanel x:Name="fruit">
        <RadioButton Content="Apple"></RadioButton>
        <RadioButton Content="Orange"></RadioButton>
        <RadioButton Content="Grape"></RadioButton>
      </StackPanel>
    </StackPanel>
    <Label x:Name="msg"  Content="  " FontSize="10pt"/>
    <Button x:Name="btn" Content="Push Me"  FontSize="10pt" />
  </StackPanel>
</Window>
  • 画面表示例:

f:id:Itsukara:20170324210102p:plain

あとがき

今回の件に関して、Webで色々と見つけたのですが、よい方法が見つからず、結局、自分で考えました。でも、後から考えると当たり前の内容であり、Webで色々調べた労力が無駄でした。

VisualStudioでPowerShell用GUIを作成する方法

PowerShellGUIを使う記事を下記2件書きましたが、PowerShellのプログラムを動かすまでGUIを確認できず不便と思っていました。もしかしたら、VisualStudioを使うと、画面設計ができなかと思ったら、簡単に出来たので報告します。

VisualStudioを使ったGUI設計

  • VisualStudioを立ち上げる。
  • ファイル⇒新規作成⇒プロジェクトを選択。

f:id:Itsukara:20170322232630p:plain

  • WPFアプリケーションを選択。

f:id:Itsukara:20170322232635p:plain

  • 下記ようなGUI設計画面になるので、左のツールボックスから部品を選んで、中央のウィンドウにドラッグして配置しGUIを設計*1

f:id:Itsukara:20170322232640p:plain

自動生成されたXAMLを使いPowerShellプログラム作成

  • 例えば下記のようにGUI部品を配置。

f:id:Itsukara:20170322232649p:plain

  • 画面下部のXAML(文字列)を抜き出してPowerShellのプログラムを作成。
  • ただし、一部の文字列は削除が必要(下記の黄色枠部分)。

f:id:Itsukara:20170322235005p:plain

  • 下記がプログラムの例。「@'」と「'@」で囲まれた部分がXAML
$ErrorActionPreference = "stop"
Set-PSDebug -Strict
Add-Type -AssemblyName PresentationFramework

[xml]$xaml = @'
<Window
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="350" Width="525">
    <Grid>
        <Label x:Name="label01" Content="名前" HorizontalAlignment="Left" Margin="15,26,0,0" VerticalAlignment="Top"/>
        <TextBox x:Name="textName" HorizontalAlignment="Left" Height="26" Margin="68,26,0,0" TextWrapping="Wrap" VerticalAlignment="Top" Width="439"/>
        <Label x:Name="label02" Content="ふりがな" HorizontalAlignment="Left" Margin="15,57,0,0" VerticalAlignment="Top"/>
        <TextBox x:Name="textFurigana" HorizontalAlignment="Left" Height="26" Margin="68,57,0,0" TextWrapping="Wrap" VerticalAlignment="Top" Width="439"/>
        <Label x:Name="label03" Content="性別" HorizontalAlignment="Left" Margin="15,88,0,0" VerticalAlignment="Top"/>
        <StackPanel x:Name="stackGender" HorizontalAlignment="Left" Height="26" Margin="68,88,0,0" VerticalAlignment="Top" Width="129" Orientation="Horizontal">
            <RadioButton x:Name="radioButtonMale" Content="Male" Height="26" VerticalAlignment="Top" HorizontalAlignment="Left" Margin="5,5,5,0"/>
            <RadioButton x:Name="radioButtonFemale" Content="Female" Height="26" VerticalAlignment="Top" HorizontalAlignment="Left" Margin="5,5,5,0"/>
        </StackPanel>
        <Label x:Name="label04" Content="国籍" HorizontalAlignment="Left" Margin="15,119,0,0" VerticalAlignment="Top"/>
        <Label x:Name="label05" Content="年齢" HorizontalAlignment="Left" Margin="15,150,0,0" VerticalAlignment="Top"/>
        <TextBox x:Name="textAge" HorizontalAlignment="Left" Height="26" Margin="68,150,0,0" TextWrapping="Wrap" VerticalAlignment="Top" Width="52"/>
        <ComboBox x:Name="comboBoxCountry" HorizontalAlignment="Left" Height="26" Margin="68,119,0,0" VerticalAlignment="Top" Width="439"/>
        <Button x:Name="buttonRegist" Content="登録" HorizontalAlignment="Left" Height="33" Margin="242,276,0,0" VerticalAlignment="Top" Width="48"/>
        <TextBlock x:Name="textBlock" HorizontalAlignment="Left" Height="90" Margin="15,181,0,0" TextWrapping="Wrap" VerticalAlignment="Top" Width="492"/>

    </Grid>
</Window>
'@

$reader = New-Object System.Xml.XmlNodeReader $xaml
$frm = [System.Windows.Markup.XamlReader]::Load($reader)

$countries = "U.S.A China Japan Germany France England Russia".split(" ")
$comboBoxCountry = $frm.FindName("comboBoxCountry")
foreach ($country in $countries) {
  [void]$comboBoxCountry.Items.Add($country)
}

$textName = $frm.FindName("textName")
$comboBoxCountry = $frm.FindName("comboBoxCountry")
$textFurigana = $frm.FindName("textFurigana")
$stackGender = $frm.FindName("stackGender")
$textAge = $frm.FindName("textAge")
$textBlock = $frm.FindName("textBlock")

function getChecked($parent) {
    $checked = ""
    foreach ($child in $parent.Children) {
        if ($child.isChecked) {
            $checked += $child.Content
        }
    }
    return $checked
}

function regist {
    $name = $textName.Text
    $furigata = $textFurigana.Text
    $gender = getChecked $stackGender
    $country = $comboBoxCountry.SelectedValue
    $age = $textAge.Text

    $msg = ""
    $msg += "名前:" + $name + "`n" 
    $msg += "ふりがな:" + $furigata + "`n" 
    $msg += "性別:" + $gender + "`n" 
    $msg += "国籍:" + $country + "`n" 
    $msg += "年齢:" + $age + "`n" 

    $textBlock.Text = $msg
}

$buttonRegist =  $frm.FindName("buttonRegist")
$buttonRegist.Add_Click({regist})

$result = $frm.ShowDialog()
  • プログラム実行例。名前等を入力後、「登録」ボタンを押したところ。

f:id:Itsukara:20170322232657p:plain

あとがき

VisualStudioを使うと、PowerShellGUIがとても簡単に設計できるので、ぜひ試してみてください。

おまけ

ListBoxやComboBoxは、PowerShellから簡単に項目を追加できるが、RadioButtonは簡単に追加できない。これはとても不便だが、次のようにプレースホルダを沢山書いておき、必要なものだけ表示するようにPowerShellで制御すれば、ある程度対応できることが分かった。ご参考まで。

  • プログラム例:
$ErrorActionPreference = "stop"
Set-PSDebug -Strict
Add-Type -AssemblyName PresentationFramework

[xml]$xaml = @'
<Window
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Title="xaml test #2"
    Width="170pt" Height="170pt">
  <StackPanel>
    <StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
      <StackPanel x:Name="country">
        <RadioButton Content="PlaceHolder"></RadioButton>
        <RadioButton Content="PlaceHolder"></RadioButton>
        <RadioButton Content="PlaceHolder"></RadioButton>
        <RadioButton Content="PlaceHolder"></RadioButton>
        <RadioButton Content="PlaceHolder"></RadioButton>
        <RadioButton Content="PlaceHolder"></RadioButton>
        <RadioButton Content="PlaceHolder"></RadioButton>
        <RadioButton Content="PlaceHolder"></RadioButton>
        <RadioButton Content="PlaceHolder"></RadioButton>
        <RadioButton Content="PlaceHolder"></RadioButton>
        <RadioButton Content="PlaceHolder"></RadioButton>
      </StackPanel>
      <Label Content="  " FontSize="10pt"/>
      <StackPanel x:Name="fruits">
        <RadioButton Content="PlaceHolder"></RadioButton>
        <RadioButton Content="PlaceHolder"></RadioButton>
        <RadioButton Content="PlaceHolder"></RadioButton>
        <RadioButton Content="PlaceHolder"></RadioButton>
        <RadioButton Content="PlaceHolder"></RadioButton>
        <RadioButton Content="PlaceHolder"></RadioButton>
        <RadioButton Content="PlaceHolder"></RadioButton>
        <RadioButton Content="PlaceHolder"></RadioButton>
        <RadioButton Content="PlaceHolder"></RadioButton>
        <RadioButton Content="PlaceHolder"></RadioButton>
        <RadioButton Content="PlaceHolder"></RadioButton>
      </StackPanel>
    </StackPanel>
    <Label x:Name="msg"  Content="  " FontSize="10pt"/>
    <Button x:Name="btn" Content="Push Me"  FontSize="10pt" />
  </StackPanel>
</Window>
'@

$reader = New-Object System.Xml.XmlNodeReader $xaml
$frm = [System.Windows.Markup.XamlReader]::Load($reader)
$country = $frm.FindName("country")
$fruits = $frm.FindName("fruits")
$msg = $frm.FindName("msg")

# 全てのRadioButtonを非表示にする
foreach ($s in $country.Children) {
    $s.Visibility = "Collapsed"
}

# 全てのRadioButtonを非表示にする
foreach ($b in $fruits.Children) {
    $b.Visibility = "Collapsed"
}

# RadioButtonのContentを設定し、表示する
$countryList = "U.S.A China Japan Germany France England Russia".split(" ")
$n = 0
$children = $country.Children
foreach ($c in $countryList) {
    $radioButton = $children[$n]
    $radioButton.Content = $c
    $radioButton.Visibility = "Visible"
    $n += 1
}

# RadioButtonのContentを設定し、表示する
$fruitsList = "Apple Orange Grape".split(" ")
$n = 0
$children = $fruits.Children
foreach ($f in $fruitsList) {
    $radioButton = $children[$n]
    $radioButton.Content = $f
    $radioButton.Visibility = "Visible"
    $n += 1
}

# 選択された項目を返す
function getChecked($parent) {
    $checked = ""
    foreach ($child in $parent.Children) {
        if ($child.isChecked) {
            $checked += $child.Content
        }
    }
    return $checked
}

function clicked {
    $selectedCountry = getChecked $country
    $selectedFruits  = getChecked $fruits
    $msg.content = "Country=" + $selectedCountry + "`n" + "Fruits=" +  $selectedFruits
}

$btn = $frm.FindName("btn")
$btn.Add_Click({clicked})
$result = $frm.ShowDialog()
  • 実行例:

f:id:Itsukara:20170323002422p:plain

*1:それぞれの部品の意味は、別のサイト等で調べてください。

PowerShell+WPFが動かない場合の原因と対策

仕事の関係でPowerShellを使ったので、PowerShellでの勉強内容と作成プログラムを記事に書きましたが、本番で動かず、困ってしまいました。同様の問題で困っている方がいるかもしれないので、解決先を書きます。

問題点

原因と解決方法

まとめ

PowerShellは、スクリプトがネットワークドライブにあるか、どのバージョンか、PowerShellPowerShell ISEか、によって動作が異なるので注意が必要。]

  • 対策:「powershell -sta -ExecutionPolicy Bypass ファイル名」で起動。
  • 別案:「powershell -sta -file ファイル名」で起動(2/18追記)。

PaintsChainerが爆速になっていた

http://itsukara.hateblo.jp/entry/2017/01/29/064926:embed:先週の記事で書いた線画着色(PaintsChanier)の件、久しぶりに最新版をダウンロードして試してみたら、当方の貧弱なGPU RAM(2GB)でも動くようになっており、サイズが500x500位の画像では3秒ぐらいで処理が完了。爆速になっていた。