はじめに
先日投稿した, F#で計算機を作ってみた記事の続編です. C#で計算機の画面を作成し, F#と連携してみました. 前回に引き続き, Ubuntu上での開発を前提にしています.
この記事において特に前提知識は問いませんが, WPFやWinformsについて知っていると面白いかもしれません.
目次
- GUIをC#で作成する: AvaloniaUI
- 画面側の実装
- F#との連携
- さいごに
- Appendix
- 参考リンク集
- 興味があれば, 全体のコード
GUIをC#で作成する: AvaloniaUI
C#でGUIを作ろうとすると, フレームワークの選択肢はいくつかあると思います. 例えば以下です
- Winforms
- WPF
- AvaloniaUI
当方, WinformsとWPFには慣れていますので, 最初は簡単にWinformsの使用を検討していました. しかし, WinformsとWPFはいずれもWindows専用です. Ubuntu上で開発していると, どうしてもプラットフォームの問題が出てきます.
そこで, クラスプラットフォーム対応のAvaloniaUIを使用してみました.
- Windows
- MacOS
- Linux
- iOS
- など
WPFっぽい書き方が出来るので, ある程度は感覚的に書くことが出来ます. ※互換性が無いことには注意です.
画面側の実装
画面側の実装について書きます. 以下はMainWindow.axamlというファイルです.
ほとんどWPFと同じノリで書いています.
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="Gui.MainWindow"
x:DataType="local:MainWindow" xmlns:local="clr-namespace:Gui"
Title="Calculator"
Width="320"
Height="520"
CanResize="False"
Background="#FF1E1E1E">
<Design.DataContext>
<local:MainWindow />
</Design.DataContext>
<Window.Styles>
<!-- 中略するが, ボタンや画面の色, フォントの設定 -->
</Window.Styles>
<Grid Margin="15">
<!-- 中略するが, ボタンや画面の配置に関する設定 -->
</Grid>
</Window>
続いて, MainWindow.axaml.csの実装です. 画面のロジックの部分ですね. ")"の直後に数値を置けないなど, 細々としたロジックを書いています. 細かくは最後に全文を公開します.
using Avalonia.Controls;
using Avalonia.Interactivity;
using Calculator;
using System;
using System.Linq;
namespace Gui;
public partial class MainWindow : Window
{
private bool _calculated;
private string _displayText = "";
// XAMLからのバインディングを想定したプロパティ
public string DisplayText
{
get => _displayText;
set
{
if (_displayText != value)
{
_displayText = value;
if (DisplayTextBox != null)
{
DisplayTextBox.Text = value;
}
}
}
}
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// =======================中略=======================
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
}
F#との連携
C#側からF#の関数を呼び出す処理です. Calculate
関数をご覧ください.
using Avalonia.Controls;
using Avalonia.Interactivity;
using Calculator;
using System;
using System.Linq;
namespace Gui;
public partial class MainWindow : Window
{
private bool _calculated;
private string _displayText = "";
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// =======================中略=======================
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
/// <summary>
/// =ボタンのクリックイベントハンドラ(計算実行)
/// </summary>
private void Calculate(object? sender, RoutedEventArgs e)
{
if (string.IsNullOrEmpty(DisplayText))
{
return; // 表示が空なら何もしない
}
try
{
string expression = DisplayText;
// 実際の計算ロジック
double result = CalculatorCore.calculateForCSharp(expression);
DisplayText += $"={result}";
_calculated = true; // 計算済み状態に設定
}
catch (Exception ex)
{
DisplayText = $"Error: {ex.Message}";
_calculated = true; // エラーも計算済み状態として扱う
}
}
}
以下の行で呼び出しを行っています.
double result = CalculatorCore.calculateForCSharp(expression);
F#側の実装を再掲します. 細かいところは以前の記事を参照してください. F#側で入出力を明示することにより, C#からF#の関数を呼び出すことが出来ます.
namespace Calculator
open Calculator.Tools.Tokenizer
open Calculator.Tools.Translator
open Calculator.Tools.Evaluator
module CalculatorCore =
/// <summary>
/// 式を計算する主関数
/// </summary>
/// <param name="expr">計算式の文字列</param>
/// <returns>計算結果</returns>
let calculate = tokenizeExpression >> translateRPN >> evaluateRPN
/// <summary>
/// C#から呼び出すための関数
/// </summary>
/// <param name="expr">計算式の文字列</param>
/// <returns>計算結果</returns>
let calculateForCSharp (expr: string) =
try
calculate expr
with
| ex -> failwithf "計算エラー: %s" ex.Message
さいごに
最後まで読んでくれてありがとうございます. C#とF#の面白さを知ってもらえたら幸いです.
Appendix
参考リンク集
興味があれば, コード全文
MainWindow.axaml
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="Gui.MainWindow"
x:DataType="local:MainWindow" xmlns:local="clr-namespace:Gui"
Title="Calculator"
Width="320"
Height="520"
CanResize="False"
Background="#FF1E1E1E">
<Design.DataContext>
<local:MainWindow />
</Design.DataContext>
<Window.Styles>
<Style Selector="Button">
<Setter Property="Margin" Value="3" />
<Setter Property="CornerRadius" Value="8" />
<Setter Property="BorderThickness" Value="0" />
<Setter Property="HorizontalContentAlignment" Value="Center" />
<Setter Property="VerticalContentAlignment" Value="Center" />
<Setter Property="HorizontalAlignment" Value="Stretch" />
<Setter Property="VerticalAlignment" Value="Stretch" />
<Setter Property="FontSize" Value="20" />
</Style>
<Style Selector="Button.number">
<Setter Property="Background" Value="#FF505050" />
<Setter Property="Foreground" Value="#FFFFFF" />
<Setter Property="FontWeight" Value="Medium" />
</Style>
<Style Selector="Button.operator">
<Setter Property="Background" Value="#FFFF9500" />
<Setter Property="Foreground" Value="#FFFFFF" />
<Setter Property="FontWeight" Value="SemiBold" />
</Style>
<Style Selector="Button.special">
<Setter Property="Background" Value="#FF3E3E42" />
<Setter Property="Foreground" Value="#FFCCCCCC" />
<Setter Property="FontWeight" Value="Medium" />
</Style>
</Window.Styles>
<Grid Margin="15">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="100" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Menu Grid.Row="0" Background="Transparent" Margin="0,0,0,10">
<MenuItem Header="Clear" Click="ClearText"
Foreground="#FFCCCCCC"
FontSize="14"
FontWeight="Medium" />
</Menu>
<Border Grid.Row="1"
Background="#FF2D2D30"
CornerRadius="12"
BorderBrush="#FF404040"
BorderThickness="1"
Margin="0,5">
<TextBox Name="DisplayTextBox"
Text="{Binding DisplayText}" IsReadOnly="True"
FontSize="24"
FontFamily="Segoe UI"
FontWeight="Light"
Background="Transparent"
BorderThickness="0"
Foreground="#FFFFFF"
TextAlignment="Right"
VerticalContentAlignment="Center"
HorizontalContentAlignment="Right"
Padding="15,0" />
</Border>
<Grid Grid.Row="2" Margin="0,15,0,0">
<Grid.RowDefinitions>
<RowDefinition Height="1*" />
<RowDefinition Height="1*" />
<RowDefinition Height="1*" />
<RowDefinition Height="1*" />
<RowDefinition Height="1*" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="1*" />
<ColumnDefinition Width="1*" />
<ColumnDefinition Width="1*" />
<ColumnDefinition Width="1*" />
</Grid.ColumnDefinitions>
<Button Grid.Row="0" Grid.Column="0" Content="⌫" Click="Backspace"
Classes="special" />
<Button Grid.Row="0" Grid.Column="1" Content="(" Click="AddLeft"
Classes="special" />
<Button Grid.Row="0" Grid.Column="2" Content=")" Click="AddRight"
Classes="special" />
<Button Grid.Row="0" Grid.Column="3" Content="/" Click="AddOperator"
Classes="operator" />
<Button Grid.Row="1" Grid.Column="0" Content="7" Click="AddNumber"
Classes="number" />
<Button Grid.Row="1" Grid.Column="1" Content="8" Click="AddNumber"
Classes="number" />
<Button Grid.Row="1" Grid.Column="2" Content="9" Click="AddNumber"
Classes="number" />
<Button Grid.Row="1" Grid.Column="3" Content="*" Click="AddOperator"
Classes="operator" />
<Button Grid.Row="2" Grid.Column="0" Content="4" Click="AddNumber"
Classes="number" />
<Button Grid.Row="2" Grid.Column="1" Content="5" Click="AddNumber"
Classes="number" />
<Button Grid.Row="2" Grid.Column="2" Content="6" Click="AddNumber"
Classes="number" />
<Button Grid.Row="2" Grid.Column="3" Content="-" Click="AddOperator"
Classes="operator" />
<Button Grid.Row="3" Grid.Column="0" Content="1" Click="AddNumber"
Classes="number" />
<Button Grid.Row="3" Grid.Column="1" Content="2" Click="AddNumber"
Classes="number" />
<Button Grid.Row="3" Grid.Column="2" Content="3" Click="AddNumber"
Classes="number" />
<Button Grid.Row="3" Grid.Column="3" Content="+" Click="AddOperator"
Classes="operator" />
<Button Grid.Row="4" Grid.Column="0" Grid.ColumnSpan="2" Content="0"
Click="AddNumber"
Classes="number" />
<Button Grid.Row="4" Grid.Column="2" Content="." Click="AddDot"
Classes="number" />
<Button Grid.Row="4" Grid.Column="3" Content="=" Click="Calculate"
Classes="operator" />
</Grid>
</Grid>
</Window>
MainWindow.axaml.cs
using Avalonia.Controls;
using Avalonia.Interactivity;
using Calculator;
using System;
using System.Linq;
namespace Gui;
public partial class MainWindow : Window
{
private bool _calculated;
private string _displayText = "";
// XAMLからのバインディングを想定したプロパティ
public string DisplayText
{
get => _displayText;
set
{
if (_displayText != value)
{
_displayText = value;
if (DisplayTextBox != null)
{
DisplayTextBox.Text = value;
}
}
}
}
public MainWindow()
{
InitializeComponent();
_calculated = false;
}
/// <summary>
/// DisplayTextを設定し, 計算済みフラグをリセットします.
/// </summary>
/// <param name="text">設定する文字列</param>
private void SetDisplayText(string text)
{
DisplayText = text;
_calculated = false; // 新しい入力があったら計算済み状態をリセット
}
/// <summary>
/// Cボタンのクリックイベントハンドラ(クリア)
/// </summary>
private void ClearText(object? sender, RoutedEventArgs e)
{
SetDisplayText("");
}
/// <summary>
/// Backspaceボタンのクリックイベントハンドラ
/// </summary>
private void Backspace(object? sender, RoutedEventArgs e)
{
if (string.IsNullOrEmpty(DisplayText))
{
return; // 表示が空なら何もしない
}
SetDisplayText(DisplayText.Remove(DisplayText.Length - 1));
}
/// <summary>
/// 左括弧ボタンのクリックイベントハンドラ
/// </summary>
private void AddLeft(object? sender, RoutedEventArgs e)
{
// 最後の文字が数値, 小数点, 閉じ括弧の場合は挿入しない
if (DisplayText.Length > 0)
{
char lastChar = DisplayText.Last();
if (char.IsDigit(lastChar) || lastChar == '.' || lastChar == ')')
{
return;
}
}
SetDisplayText(DisplayText + "(");
}
/// <summary>
/// 右括弧ボタンのクリックイベントハンドラ
/// </summary>
private void AddRight(object? sender, RoutedEventArgs e)
{
if (string.IsNullOrEmpty(DisplayText))
{
return; // 表示が空なら何もしない
}
char lastChar = DisplayText.Last();
int leftCount = DisplayText.Count(c => c == '(');
int rightCount = DisplayText.Count(c => c == ')');
// 最後の文字が数値または閉じ括弧で, かつ左括弧の数が多い場合にのみ追加
if ((char.IsDigit(lastChar) || lastChar == ')') && leftCount > rightCount)
{
SetDisplayText(DisplayText + ")");
}
// それ以外の場合は何もしない
}
/// <summary>
/// 数字ボタンのクリックイベントハンドラ
/// </summary>
private void AddNumber(object? sender, RoutedEventArgs e)
{
if (sender is not Button button || button.Content is not string buttonText)
{
return;
}
if (_calculated) // 計算結果が表示されている場合はクリアして新しい入力を開始
{
SetDisplayText(buttonText);
}
else
{
// 最後の文字が閉じ括弧の場合は, 数字を追加しない(例: "2)" の後に "3" とはならない)
if (DisplayText.Length > 0 && DisplayText.Last() == ')')
{
return;
}
SetDisplayText(DisplayText + buttonText);
}
}
/// <summary>
/// 演算子ボタンのクリックイベントハンドラ
/// </summary>
private void AddOperator(object? sender, RoutedEventArgs e)
{
if (sender is not Button button || button.Content is not string buttonText)
{
return;
}
if (string.IsNullOrEmpty(DisplayText))
{
return; // 表示が空なら何もしない
}
char lastChar = DisplayText.Last();
// 最後の文字が数字または閉じ括弧の場合のみ演算子を追加
if (char.IsDigit(lastChar) || lastChar == ')')
{
// 既に計算結果が表示されている場合でも演算子は追加可能
SetDisplayText(DisplayText + buttonText);
}
// それ以外の場合は何もしない
}
/// <summary>
/// 小数点ボタンのクリックイベントハンドラ
/// </summary>
private void AddDot(object? sender, RoutedEventArgs e)
{
if (sender is not Button button || button.Content is not string buttonText)
{
return;
}
if (string.IsNullOrEmpty(DisplayText))
{
return; // 表示が空なら何もしない
}
char lastChar = DisplayText.Last();
// 最後の文字が数字でない場合は追加しない
if (!char.IsDigit(lastChar))
{
return;
}
// 現在の数値部分にすでに小数点が含まれているかチェック
bool hasDotInCurrentNumber = false;
for (int i = DisplayText.Length - 1; i >= 0; i--)
{
char c = DisplayText[i];
if (c == '.')
{
hasDotInCurrentNumber = true;
break;
}
// 数字, 括弧, 演算子以外の文字(例: スペース, 未定義の文字)が現れたら, 現在の数値部分の終わりと判断
if (!char.IsDigit(c))
{
// ここで現在の数値部分が終了したと判断し, 小数点が見つからなければ追加可能
break;
}
}
if (!hasDotInCurrentNumber)
{
SetDisplayText(DisplayText + buttonText);
}
// 既に小数点が含まれている場合は何もしない
}
/// <summary>
/// =ボタンのクリックイベントハンドラ(計算実行)
/// </summary>
private void Calculate(object? sender, RoutedEventArgs e)
{
if (string.IsNullOrEmpty(DisplayText))
{
return; // 表示が空なら何もしない
}
try
{
string expression = DisplayText;
// 実際の計算ロジック
double result = CalculatorCore.calculateForCSharp(expression);
DisplayText += $"={result}";
_calculated = true; // 計算済み状態に設定
}
catch (Exception ex)
{
DisplayText = $"Error: {ex.Message}";
_calculated = true; // エラーも計算済み状態として扱う
}
}
}