Getting a Good Grasp of F# (仮)

関数型言語F#をもっと楽しみたい

開発環境メモ - VS 2019 Preview 2.2 のタイトルバーを復活させる

[ 2019.06.30 追記:Visual Studio 2019 Preview 3.0 ではタイトルバーの有無を選択するメニュー項目が出来ました。 ]

 

Visual Studio 2019 Preview 2.2 には、以前のバージョンに存在した設定項目「ツール → オプション → 環境 → Preview Features 」が存在しません。

もしVisual Studio 2019 Preview 2.2 のタイトルバーが消えていてこれを復活させたいとなると、現状では直接テキストエディタで設定ファイル( CurrentSettings.vssettings )を書き替えて設定変更を行うしかなさそうです。(作業に失敗した場合に備えてこの作業に入る前に元の CurrentSettings.vssettings を念のため保存しておきましょう)

 CurrentSettings.vssettings が置かれているディレクトリは(本記事を書いた時点で)
[LOCALAPPDATA]\Microsoft\VisualStudio\16.0_7a5e868f\Settings\
となっていました。[LOCALAPPDATA] の部分はユーザ環境に依存する部分で、環境変数 LOCALAPPDATA で示される文字列です。「16.0_7a5e868f」の部分は今後 Visual Studio のバージョンによって変化すると思われる部分です。

ちなみに環境変数 LOCALAPPDATA の値は
コマンドプロンプトを使って echo %localappdata% で確認できます。

f:id:pongitsune:20190207201329p:plain

PowerShell コマンドを使った場合 $env:localappdata で確認できます。


Visual Studio 2019 Preview 2.2 を終了させてから 前述のディレクトリ中にある CurrentSettings.vssettings をテキストエディタで開いて、<PropertyValue name="IsMinimalVsEnabled">True</PropertyValue> と書かれている箇所の値(True)を以下の画像のように False に書き替えます。

 CurrentSettings.vssettings 変更後に Visual Studio 2019 Preview 2.2 を起動すると幅の広いタイトルバーが現れます。

 

WPF のバインディングソースとなるクラスを F# で作る

WPF にはデータバインディングによってオブジェクトの状態変化を別のオブジェクトへ通知する機能があります。 本記事ではバインディングソースを F# のクラスとして作り WPF コントロールと連携させます。そのために INotifyPropertyChanged インタフェースと F# のイベント通知の機能を用います。

本記事では、Window 上の TextBlock コントロールの描画位置をDispatcherTimer のタイマーイベントで定期的に更新して回転させるプログラムを作ります。

F# プロジェクトの作成

F#の新規プロジェクトとして「コンソールアプリケーション(.NET Framework」を選択します。

WPF アプリケーションとして作成するので「プロジェクトのプロパティ」の設定画面で「出力の種類」を「Windows アプリケーション」に変更します。

アセンブリ参照の追加

WPF を利用するために必要なアセンブリ参照を追加します。

  • WindowsBase
  • PresentationCore
  • PresentationFramework
  • System.Xaml

F# のソースコード(Program.fs)

  1. open System.ComponentModel
  2. open System.Runtime.CompilerServices
  3. open System
  4. open System.Windows.Threading
  5. open System.Windows.Markup
  6. open System.Windows
  7.  
  8. let xamlText =
  9.     """
  10.     <Window
  11.         xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  12.         xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  13.         Title="SimpleDataBinding"  SizeToContent="WidthAndHeight"
  14.         ResizeMode="NoResize" WindowStartupLocation="CenterScreen">
  15.  
  16.         <Canvas Width="300" Height="300">
  17.             <TextBlock Text="回転します" Foreground="Red" FontSize="30" Canvas.Left="150" Canvas.Top="150">
  18.                 <TextBlock.RenderTransform>
  19.                     <RotateTransform CenterX="0" CenterY="0" Angle="{Binding Path=Degree}" />
  20.                 </TextBlock.RenderTransform>
  21.             </TextBlock>
  22.         </Canvas>
  23.     </Window>
  24.     """
  25.  
  26. type BaseNotifyer() =
  27.     let propertyChangedEvent =
  28.             new Event<PropertyChangedEventHandler,PropertyChangedEventArgs>()
  29.  
  30.     interface INotifyPropertyChanged with
  31.         [<CLIEvent>]
  32.         override __.PropertyChanged = propertyChangedEvent.Publish
  33.  
  34.     member this.OnPropertyChanged([<CallerMemberName>]?propertyName: string) =
  35.                         let args = new PropertyChangedEventArgs(Option.defaultValue "" propertyName)
  36.                         propertyChangedEvent.Trigger(this, args)
  37.  
  38. type MyBindingSource() =
  39.     inherit BaseNotifyer() with
  40.     let mutable degree = 0.0
  41.     member this.Degree
  42.         with get(): double = degree
  43.         and set(value) =
  44.             degree <- value
  45.             this.OnPropertyChanged()
  46.  
  47. [<EntryPoint; STAThread>]
  48. let main _ =
  49.     let dpTimer = new DispatcherTimer(Interval=TimeSpan.FromMilliseconds(100.0))
  50.     let bindingSrc = MyBindingSource()
  51.  
  52.     let eventHandler(_: EventArgs) =
  53.         let step = 10.0
  54.         if bindingSrc.Degree + step >= 360.0 then
  55.             bindingSrc.Degree <- 0.0
  56.         else
  57.             bindingSrc.Degree <- bindingSrc.Degree + step
  58.  
  59.     dpTimer.Tick.Add(eventHandler)
  60.  
  61.     let window = XamlReader.Parse(xamlText) :?> Window
  62.     window.DataContext <- bindingSrc
  63.     dpTimer.Start()
  64.  
  65.     Application().Run window

 ソースコードの説明

8~24行目: Window 上に Canvas コントロールを配置し、その上に TextBlock コントロールを表示するXAMLコードとなる文字列データです。

  1. <TextBlock Text="回転します" Foreground="Red" FontSize="30" Canvas.Left="150" Canvas.Top="150">
  2.     <TextBlock.RenderTransform>
  3.         <RotateTransform CenterX="0" CenterY="0" Angle="{Binding Path=Degree}" />
  4.     </TextBlock.RenderTransform>
  5. </TextBlock>

17行目の TextBlock コントロールの RenderTransform プロパティに設定されている RotateTransform オブジェクトの Angle プロパティ(19行目)をバインディングターゲットとしています。 Angle プロパティは、バインディングソースである MyBindingSource オブジェクト内のDegreeプロパティ更新時に発生する PropertyChanged イベント通知に応じて値を更新します。

 Angle プロパティは TextBlock コントロール描画時に使われる(時計回り方向の)回転移動角度であり double  型の値です。 Canvas コントロール上の座標(150, 150)を中心として TextBlock コントロールの文字列を Angle プロパティで指定した角度だけ(見かけ上)回転移動させ描画します。


 

26~36行目:BaseNotifyer クラスは INotifyPropertyChanged インタフェースを実装してバインディングソースとなるために必要な最小限の機能を持たせたクラスです。再利用可能なクラスとして最小限の機能を定義しています。

  1. let propertyChangedEvent =
  2.         new Event<PropertyChangedEventHandler,PropertyChangedEventArgs>()

F# には event キーワードがないので C# のようなイベント構文は使えません。Event クラスを用いて PropertyChanged イベントの通知機能を作ります。

  1. interface INotifyPropertyChanged with
  2.     [<CLIEvent>]
  3.     override __.PropertyChanged = propertyChangedEvent.Publish

 PropertyChanged プロパティには CLIEvent 属性を付加します。これにより他の .NET 言語からも利用可能なイベントを定義出来ます。

  1. member this.OnPropertyChanged([<CallerMemberName>]?propertyName: string) =
  2.                     let args = new PropertyChangedEventArgs(Option.defaultValue "" propertyName)
  3.                     propertyChangedEvent.Trigger(this, args)

34行目の '?' が付いた オプショナル引数 propertyName には、付加された CallerMemberName 属性によって呼び出し側メソッド名(あるいはプロパティ名)が string option 型の値として自動的に割り当てられます。36行目Event クラスの Trigger メソッドでイベントを発生させます。


 

38~45行目:MyBindingSource クラスはバインディングソースとなるクラスで BaseNotifyer クラス(26~36行にて宣言)の派生クラスです。XAMLコード内の TextBlock コントロールに与える回転角度の値をDegree プロパティとして持っています。

  1. type MyBindingSource() =
  2.     inherit BaseNotifyer() with
  3.     let mutable degree = 0.0
  4.     member this.Degree
  5.         with get(): double = degree
  6.         and set(value) =
  7.             degree <- value
  8.             this.OnPropertyChanged()

41行目の Degree プロパティは(set メソッド経由で)値が書き変えられた直後に OnPropertyChanged メソッドを呼び出す(45行目)ことにより、バインディングターゲットに対して PropertyChanged イベントを発生させます。

45行目で基底クラス(BaseNotifyer)の OnPropertyChanged メソッドを(引数を省略して)呼び出しています。 このメソッドを呼び出す側である Degree プロパティの名前 "Degree" が自動的に OnPropertyChanged メソッドの引数として渡ります。


 

52~57行目: DispatcherTimerイベントハンドラとなる関数 eventHandler の定義です。

  1. let eventHandler(_: EventArgs) =
  2.     let step = 10.0
  3.     if bindingSrc.Degree + step >= 360.0 then
  4.         bindingSrc.Degree <- 0.0
  5.     else
  6.         bindingSrc.Degree <- bindingSrc.Degree + step
  7.  
  8. dpTimer.Tick.Add(eventHandler)

関数 eventHandler は呼び出される度に、イベントソースである MyBindingSource オブジェクトのプロパティ Degree を0.0から360.0まで10.0刻みで変化さています。59行目DispatcherTimer オブジェクトにイベントハンドラとして登録されています。このイベントハンドラは、49行目で DispatcherTimer オブジェクトの Interval プロパティを100ミリ秒に指定したので100ミリ秒間隔で呼び出され Degree プロパティの値を更新します。


 

 61行目XamlReader によりXAML文字列を Window オブジェクトへ変換します。

  1. let window = XamlReader.Parse(xamlText) :?> Window
  2. window.DataContext <- bindingSrc
  3. dpTimer.Start()

62行目XAMLコードから生成された Window オブジェクトの DataContext プロパティにバインディングソースとなる MyBindingSource オブジェクトを代入しています。これにより XAML コード内の TextBlock コントロールのプロパティと MyBindingSource オブジェクトのプロパティの間でイベント通知が可能になります。

WPFの仕様上、 DataContext プロパティ値への参照は Window オブジェクトからその子要素である TextBlock コントロールへと継承される仕組みなので、 TextBlock コントロール内のプロパティはバインディングターゲットとして PropertyChanged イベントを受け取ることができます。

63行目DispatcherTimer オブジェクトを始動させています。これ以降周期的にタイマーイベントが発生します。 

開発環境メモ - VS 2019 preview 起動時の挙動・外観を 2017 に近い状態に戻す

[ 2019.02.07 追記:Visual Studio 2019 Preview 2.2 で確認したところ本記事で説明している設定項目はなくなっていました。タイトルバーの設定についてはこちらの記事を参照のこと。]

Visual Studio 2019 preview 版導入にあたって、起動直後の UI 設定を 2017 風に戻すために設定変更をしてみました。

2019 preview 版の以下の UI 設定を変更します。(その後、必要ならば簡単に設定を元に戻せます)

  • 2017 に比べてタイトルバーが狭くなった
  • 「スタートページ」が独立したウィンドウになった
  • 「プロジェクトの新規作成」ウィザードが変わった

 Visual Studio 2019 preview 版をインストールして起動すると 2017 の頃とは挙動が異なっており、スプラッシュウィンドウが消えた直後は以下のように、 

スタートページが独立したウィンドウになって表示されます。Visual Studio 本体はまだ起動しません。
そして以下のようにこのウィンドウ右上の×ボタンを押すと

Visual Studio 2019 は本体を起動せずに即終了してしまいます。

とりあえず Visual Studio 本体の起動(プロジェクト/ソリューションを何も開いていない状態)だけしたい場合は、

画面右下の Continue without code をクリックします。そうすれば Visual Studio 本体が起動した状態になります。


インストール直後の状態では のタイトルバーが  2017 より小さくなるなど新機能がいくつか個人的には操作が不便と感じる部分があったたので UI 設定を変更することにしました。

下の画像は 2017(左)と 2019(右)のタイトルバー比較。

メニューバーから「ツール」→「オプション」項目を選択

 →「環境」→「Preview Features」項目へと進みます。

  • Use compact menu and search bar (require restart) → タイトルバー周辺を縮小さくする設定
  • Show search and filter options for creating new projects → 今回新しくなった「プロジェクト新規作成」ウィザード画面を出す設定
  • Show start window for opening code on startup (require restart)  → スタートページを独立のウィンドウでを本体起動前に出す設定

それぞれチェックボックスOFF にします。再起動の必要がある項目もあるので OK ボタンで確定した後に Visual Studio を終了させて再度起動させて設定を反映させます。

本体が直接起動し、スタートページは 2017と同様にタブページ上に収まっています。タイトルバーも広がりました。

下の画像は 2017(左)と 設定変更後の2019(右)のタイトルバー比較。

 

WPF で NumericUpDown コントロールを使う その2 [F#編]

前回 C# で実装した機能を今記事では F# で実装してみます。前回書いた XAML コードを(文字列としてですが)再利用します。

F# での実装

F# でプロジェクトの作成

F#の新規プロジェクトを作成するためにまず「コンソールアプリケーション(.NET Framework)」を選択します。プロジェクト名は任意で構いません(本記事では「 WPF_SpinFs 」としています)。

WPF アプリケーションとして作成するので「プロジェクトのプロパティ」の設定画面で「出力の種類」を「Windowsアプリケーション」に変更します。

 アセンブリ参照の追加

WPF を利用するために必要なアセンブリ参照を追加します。

  • WindowsBase
  • PresentationCore
  • PresentationFramework
  • System.Xaml

WinForms に属するコントロールを使うため、必要なアセンブリ参照を追加します。

  • System.Windows.Forms
  • WindowsFormsIntegration

 今記事では XAMLXMLドキュメントとして処理するため以下のアセンブリ参照を追加します。

 プロジェクトが参照するアセンブリ一覧は以下のようになります。

F# のソースコード

前回用いた XAML コードをコピー&ペーストして流用しています。ただし C# のプロジェクトで使っていた XAML をそのまま F# で XamlReader によって読み込こむことはできません。前処理として、 XAML コードを XML 文書として読み込み、動的に不要な情報を削除し必要な情報を追加しています。 

15~38行目前回 C#のプロジェクトで用いた XAML コードを過不足なくコピー&ペーストしただけの部分です。C# にはない文法ですが、文字列の前後を3連続するダブルクオート( """ )で囲んでいます。エスケープシーケンスを使うことなく文字列内にダブルクオート( " )を置くことができ、また複数行に渡る文字列を扱えます。

手作業で F# 向けに XAML の内容を修正してもよいのですが、今記事では後で適正な XAML の形へ動的に変更を加えることにします。

44行目XElement クラス (System.Xml.Linq) を使って XAML コードを XMLとして読み込んでいます。

46~48行目XAML のルート要素の x:Class 属性(C# のコードビハインドのクラス名)を探し出して削除しています。

50~53行目XAML のルート要素に名前空間を追加し、XAML から名前空間 System.Windows.Forms.Integration への参照を可能にします。

C# では XName 型と string 型の間の暗黙の型変換が行われるのですが F# ではこの暗黙の型変換が機能しないので XName.Get メソッドを用いて明示的な型変換を行っています。

55行目:(前処理の済んだ) XElement オブジェクトが保持する XML ドキュメントを  XamlReader が XAML 文字列として読み込んで WPF の Window オブジェクトに変換しています。

実行結果

前回C# のコードと同じ振る舞いを実現できました。

WPF で NumericUpDown コントロールを使う その1 [C#編]

WPF には Spin コントロールがありません。一方で、WinForms には Spin コントロールとしての機能を持つ NumericUpDown クラス が存在します。C#WPF の Window 上にこの NumericUpDown コントロールを配置してみます。このとき WPF に WinForms コントロールを混在させるために WindowsHost クラスを利用します。後に F# でも実装を試みる予定です。

NumericUpDownコントロールの例

C#での実装

C#WPF プロジェクトの作成

C#の新規プロジェクトをWPFアプリケーション用に作成します。今記事ではプロジェクト名は「WPF_SpinControlCs」にしています。

C#でWPFアプリの新規プロジェクトを作成

アセンブリ参照の追加

まず、WinForms に属するコントロールを使うため、必要なアセンブリ参照(以下2つ)をプロジェクトに追加します

    • System.Windows.Forms
    • WindowsFormsIntegration

プロジェクトの参照するアセンブリ一覧は以下のようになったはずです。

XAMLファイルの書き替え

デフォルトで生成された MainWindow.xaml のコードを以下のように書き替えます。今記事ではコードビハインド( MainWindow クラスのC#コード)には一切手を加えていません。

7行目アセンブリ System.Windows.Forms の NumericUpDown クラス を参照するために XAMLコードのルート要素( Window )において、新たな属性として、名前空間URIを追加しました。接頭辞(prefix)は、ここでは仮に wfm と設定しましたが、任意の識別子が使えます。

注意:名前空間に指定するURI文字列にホワイトスペースを混在させないこと

16行目WindowsFormsHost クラスは WPF のコントロール上に NumericUpDown コントロールを配置するために必要なラッパーとして機能します。

18行目: NumericUpDown コントロールを配置するための要素です。7行目で宣言した接頭辞 wfm を付けた形( wfm:NumericUpDown )で参照します。x:Name 属性は必須です。コントロールのプロパティを、初期値:5、最大値:10、最小値:0、ユーザによる編集は不可( ReadOnly="True" )として設定してあります。

ただし、上記のようなXAMLコードを書いても、XAMLデザイナーのデザインビューでは NumericUpDown のような WinForms に属するコントロールの外観を見ることはできません。以下のような灰色に塗りつぶされた表示になってしまいます。

21行目WPF TextBlock コントロールです。NumericUpDown コントロールValue プロパティと 自身の Text プロパティとの間でデータバインディングを行い、最新の値を表示します。

実行結果

コンパイルして実行すると以下のようなウィンドウが表示されます。もちろん、上下ボタンを押せばコントロール上の数値が増減します。XAMLでプロパティ設定した通り、上限・下限に設定した値を超えることもありません。WPF TextBlock コントロールにも値の更新が反映されています。

f:id:pongitsune:20181123221521p:plain