L'Isola di Niente
L'Isola di Niente » .NET Tips » IronPython で WPF

IronPython で WPF

WPF を IronPython から使う方法。

2011-05-03 IronPython 2.7 (.NET 4.0 専用なので注意) コードに書き換え

注意

定数はすべて大文字、型名はアッパーキャメルケース、それ以外は全部小文字でスネークケース
いまさら聞けない「変数の命名規則」 - プログラマ 福重 伸太朗 ~基本へ帰ろう~
そしてインデントは半角スペース 4 つ
という Python の掟で書いています、名の知れた Python コードは大半がそうなっているはずです。
なので VC++ 使いや C# 屋の御仁には少し見にくいかもしれません。

コードで作成

PresentationCore.dll, PresentationFramework.dll, WindowsBase.dll
へのリファレンス参照が WPF を使う場合には必要、System.dll も必要だがコレは暗黙で行われる。

リファレンス参照とは VC# 等を持っている人なら解るだろうけどソリューションエクスプローラにある参照設定以下のこと。
VC# なら右クリックして「参照の追加」をする、コンパイルを行わない IronPython はコードで行う。

img/vcs.png

2.7 からは以下のように import wpf と書けばリファレンス参照が自動で行われるようです。
2.6 には wpf モジュールが存在しないのでエラーになります。

では WPF ウインドウを表示するだけな最小限コード
# -*- coding: UTF-8 -*-

import clr
import wpf

"""
# IronPython 2.6 の場合はこう書く
clr.AddReferenceByPartialName("PresentationCore")
clr.AddReferenceByPartialName("PresentationFramework")
clr.AddReferenceByPartialName("WindowsBase")
"""

import System

w = System.Windows.Window()
w.Title = "実験"
a = System.Windows.Application()
a.Run(w)


コレだけで ipyw.exe なら 32bit、ipyw64.exe なら 64bit で動くウインドウ完成です。
DLR は初期化が恐ろしく遅いという宿命があるので表示されるまで気長にお待ちください。

以後は from 文を使います、C# の using と少し意味合いが違うので注意して使いましょう。
Window や Application というクラスは System.Windows 名前空間にあります。
知っていると思うけど Python の名前空間はモジュール名そのままである。

上記を拡張しても後々で困るのでさっさとクラスに変更します。
ソースコード保存時の文字コードは先頭の coding 指定に必ず合わせてください。
# -*- coding: UTF-8 -*-

import clr
import wpf

from System import *
from System.Windows import *

TITLE = "class の実験"

class TestWin(Window):
    def __init__(self):
        self.Title = TITLE
        self.Closing += self.on_closing
    
    def on_closing(self, sender, e):
        r = MessageBox.Show(
                "終了する?",
                TITLE,
                MessageBoxButton.YesNo,
                MessageBoxImage.Warning )
        if r == MessageBoxResult.No:
            e.Cancel = True

if __name__ == "__main__":
    w = TestWin()
    a = Application()
    a.Run(w)


C# にてコードのみのウインドウを作った人なら解るとおり C# とほとんど同じに書けます。
Python のクラスなのでメソッドの引数に必ず self が必要です。

VC# で作るのと同じように Window を継承したクラスを __main__ で作る。
最後に Application インスタンスの Run() にぶち込めばクラス化は完了である。
これで C# コードからの変換が簡単になりました。

ついでに終了時にダイアログを出すようにハンドラを追加してみた。
「いいえ」を選択すると終了しないのが確認できます。

次にコントロールを追加してみます。
レイアウタ(ここでは StackPanel)を利用してコントロールを配置していきます。
# -*- coding: UTF-8 -*-

import clr
import wpf

from System import *
from System.Windows import *
from System.Windows.Controls import *

class TestWin(Window):
    def __init__(self):
        self.Title = "Controls"
        # ボタン作成及びクリックハンドラ登録
        button = Button()
        button.Content = "ボタンだよ"
        button.Click += self.on_click
        # 一行エディットの作成
        # 内容を他で使うのでアトリビュートにしておく
        self.textbox = TextBox()
        self.textbox.Text = "なんか書け"
        # TextBlock の作成
        tb = TextBlock()
        tb.Text = "何か書き込んでボタンを押してね"
        # レイアウタを作成してこれらをセット
        sp = StackPanel()
        sp.Orientation = Orientation.Vertical
        sp.Children.Add(button)
        sp.Children.Add(self.textbox)
        sp.Children.Add(tb)
        # レイアウタを Window にセット
        self.Content = sp
        # この指定でコンテンツの大きさ固定なウインドウになる
        self.SizeToContent = SizeToContent.WidthAndHeight
        self.ResizeMode = ResizeMode.NoResize
    
    def on_click(self, sender, e):
        MessageBox.Show(self.textbox.Text)

if __name__ == "__main__":
    Application().Run(TestWin())


Button 等のクラスは System.Windows.Controls にあるので import する。
Window や Button に何かを乗せるには Content プロパティに代入する。
レイアウタにコントロールを追加するには Children.Add() メソッドを利用する。
という簡単な例である、最後に Run() の中で Window を作る小技をば。

img/window.png

XAML の読み込み

とりあえずオーソドックスな XAML ファイルの読み込み方法。

以下の xml コードを xamltest.xaml という名前で保存します、必ず UTF-8 で保存してください。
BOM の有無は吸収してくれます(だから良くないんだが...)
<Window
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Title="タイトル" Height="240" Width="320" xml:lang="ja-JP">
    <StackPanel>
        <TextBlock>はろぉわーるど</TextBlock>
    </StackPanel>
</Window>


xmlns の指定は必須です、この指定で PresentationFramework 等が参照できるようになる。
コードで書く clr.AddReferenceByPartialName と同じだと思えばいい。
XAML 名前空間は .NET 3.5 での指定でしたが .NET 4.0 でも変わっていませんでした。

ここでは xml:lang を指定していますが通常は不要のはずです。
コンパイルを行う C# では問題無いですが IronPython ではマレに文字化けが起こる場合があります。
文字化けが起こった場合はコレを追記すればイケるようなのでここでは一応書いておきます。

ということでコレを読み込んで表示する最小限のコードを書いてみます。
# -*- coding: UTF-8 -*-

import clr

clr.AddReferenceByPartialName("PresentationFramework")

from System import *
from System.Windows import *
from System.Windows.Markup import XamlReader
from System.IO import *

xaml_path = Path.Combine(Path.GetDirectoryName(__file__),"xamltest.xaml")
fs = FileStream(xaml_path, FileMode.Open, FileAccess.Read)
w = XamlReader.Load(fs)
fs.Close()
Application().Run(w)


img/hello_xaml.png

複雑な XAML ファイルを利用するならこんな感じで読み込みを行います。

注意しなければいけないことは IronPython からはイベントハンドラは XAML 中で指定できません。
後で説明するようにコードでハンドラのコネクトを行う必要があります。

しかし単純なアプリに場合はいちいち別ファイルを読み込むのも何か無駄を感じます。
そういう場合は Python ならではの docstring を利用しましょう。
# -*- coding: UTF-8 -*-

import clr
import wpf

from System.Windows import Application
from System.Windows.Markup import XamlReader

xaml = """<Window
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    SizeToContent="WidthAndHeight" Title="コピーテキスト">
    <StackPanel>
        <TextBlock Text="何か書き込むと下にそのまんまコピーされる" />
        <TextBox Name="_tname" />
        <TextBlock Text="{Binding ElementName=_tname, Path=Text}" />
    </StackPanel>
</Window>"""

w = XamlReader.Parse(xaml)
Application().Run(w)


img/copytext.png

XamlReader の Parse メソッドで簡単に XAML が読み込みできるしファイルも一つで終わりです。
ついでに XAML でデータバインディングもやっている、問題なく動作するのが確認できる。
これでコードもスッキリ。

部品の一部だけを XAML にする

XAML で Window を作るというと全部の部品を XAML にしなければいけない錯覚が起こる。
GTK+ での GtkUIManager のようにメニューやツールバーのみ XML にすれば綺麗に作れるのに。

なんて思う人もいるでしょう、実際一部部品のみを XAML にすることは可能です。
ただし上記のように XAML 毎に xmlns 指定が必要です。

イベントハンドラのコネクトは以下のように行います。
Menu は ItemControl 派生クラスなので FindName メソッドが利用できるようだ。
メニュー全部に Name プロパティを指定する必要があるけど。

ということで単純なテキストエディタを作ってみる。
エンコード指定を行わないストリームでの読み書きはすべて BOM 無し UTF-8 となります。
# -*- coding: UTF-8 -*-

"""
    This Code is Suitable for Japanese
    Read and Write encoding is UTF-8 Non BOM Text
"""

import clr
import wpf

from System import *
from System.IO import *
from System.Windows import *
from System.Windows.Controls import *
from System.Windows.Input import *
from System.Windows.Controls.Primitives import *
from System.Windows.Markup import XamlReader
import Microsoft

menu_str = """<Menu
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
    <MenuItem Header="_File">
        <MenuItem Header="_Open" InputGestureText="Ctrl+O" Name="menu_open" />
        <MenuItem Header="_Save" InputGestureText="Ctrl+S" Name="menu_save" />
        <MenuItem Header="Save _As" Name="menu_save_as" />
        <Separator/>
        <MenuItem Header="_Quit" InputGestureText="Ctrl+Q" Name="menu_close" />
    </MenuItem>
</Menu>"""

class WPF_TextEditor(Window):
    """
        Simple TextEditor
    """
    def __init__(self):
        """
            Initialization
        """
        self.openfile = ""
        # Menu
        menu = XamlReader.Parse(menu_str)
        menu.FindName("menu_open").Click += self.on_open
        menu.FindName("menu_save").Click += self.on_save
        menu.FindName("menu_save_as").Click += self.on_save_as
        menu.FindName("menu_close").Click += self.on_close
        DockPanel.SetDock(menu, Dock.Top)
        # TextBox
        self.textbox = TextBox()
        self.textbox.TextWrapping = TextWrapping.NoWrap
        self.textbox.AcceptsReturn = True
        self.textbox.VerticalScrollBarVisibility = ScrollBarVisibility.Auto
        self.textbox.HorizontalScrollBarVisibility = ScrollBarVisibility.Auto
        # StatusBar
        statusbar = StatusBar()
        DockPanel.SetDock(statusbar, Dock.Bottom)
        self.statusbar_item = StatusBarItem()
        self.statusbar_item.Content = "no open"
        statusbar.Items.Add(self.statusbar_item)
        # DockPanel
        dpanel = DockPanel()
        dpanel.Children.Add(menu)
        dpanel.Children.Add(statusbar)
        dpanel.Children.Add(self.textbox)
        # self
        self.Content = dpanel
        self.Title = "WPF_TextEditor"
        # event
        self.KeyDown += self.on_keydown
        # Drag and drop
        self.AllowDrop = True
        self.PreviewDragOver += self.on_predrop
        self.Drop += self.on_drop

    def on_open(self, sender, e):
        """
            namespace Microsoft Dialog
        """
        dlg = Microsoft.Win32.OpenFileDialog()
        if dlg.ShowDialog(self):
            self.read_file(dlg.FileName)

    def on_save(self, sender, e):
        """
            is Open ?
        """
        if self.openfile == "":
            self.on_save_as(sender, e)
        else:
            self.save_file(self.openfile)

    def on_save_as(self, sender, e):
        """
            save
        """
        dlg = Microsoft.Win32.SaveFileDialog()
        dlg.Title = "SaveFile"
        if dlg.ShowDialog():
            self.save_file(dlg.FileName)

    def on_close(self, sender, e):
        """
            Bye
        """
        self.Close()

    def on_keydown(self, sender, e):
        """
            InputGestureText is Show only
            e is System.Windows.Input.KeyEventArgs
        """
        if Keyboard.Modifiers == ModifierKeys.Control:
            if e.Key == Key.O:
                self.on_open(self, e)
            elif e.Key == Key.S:
                self.on_save(self, e)
            elif e.Key == Key.Q:
                self.on_close(self, e)

    def on_predrop(self, sender, e):
        """
            PreviewDragOver Event
        """
        e.Handled = e.Data.GetData(DataFormats.FileDrop) != None

    def on_drop(self, sender, e):
        """
            File Dropped
        """
        filenames = e.Data.GetData(DataFormats.FileDrop)
        filename = filenames[0].ToString()
        if filename != None and filename.Length != 0:
            self.read_file(filename)

    def read_file(self, filename):
        """
            f = open(filename)
            try:
                s = f.read()
                self.textbox.Text = unicode(s, "utf8")
            except Exception, ex:
                MessageBox.Show(ex.Message)
            finally:
                f.close()
        """
        sw = StreamReader(filename)
        try:
            self.textbox.Text = sw.ReadToEnd()
            self.statusbar_item.Content = Path.GetFileName(filename)
            self.openfile = filename
        except Exception, ex:
            MessageBox.Show(ex.Message)
        finally:
            sw.Close()

    def save_file(self, filename):
        """
            f = open(filename, "w")
            try:
                f.write(self.textbox.Text.encode("utf8"))
            except Exception, ex:
                MessageBox.Show(ex.Message)
            finally:
                f.close()
        """
        sw = StreamWriter(filename)
        try:
            buf = self.textbox.Text
            sw.Write(buf)
            self.statusbar_item.Content = Path.GetFileName(filename)
            self.openfile = filename
        except Exception, ex:
            MessageBox.Show(ex.Message)
        finally:
            sw.Close()

if __name__ == "__main__":
    Application().Run(WPF_TextEditor())


img/homu.png

.NET 4.0 の恩恵で Microsoft 名前空間ダイアログが Windows 7 標準になります。
OpenFileDialog クラス (Microsoft.Win32)

こういうふうに作れると気持ちがイイと思うのは私だけなのだろうか。
Visual Studio にて C# で作っていると多分こんなことをやろうなんて思いつかないでしょう。
Copyright(C) sasakima-nao All rights reserved 2002 --- 2017.