2
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

C#で計算機の画面部分を作ってみた, あるいはC#とF#の連携について

Posted at

はじめに

先日投稿した, 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; // エラーも計算済み状態として扱う
        }
    }
}
2
3
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?