English Русский 中文 Español Deutsch Português
最適化管理 (パート I): GUI の作成

最適化管理 (パート I): GUI の作成

MetaTrader 5テスター | 10 9月 2019, 07:54
1 440 0
Andrey Azatskiy
Andrey Azatskiy

目次

イントロダクション

MetaTraderターミナルを起動するための代替方法は、Vladimir Karputovによって記事で既に考察されました。 また、ターミナルの起動ステップと追加の代替方法については、関連 ドキュメントで説明します。 この記事では、 2 つのソースからのデータが使用されましたが、複数のターミナルを同時に操作するための便利な GUI を作成するメソッドの説明は、どのソースも含まれていません。 このトピックは、この記事で説明します。

関連する研究に基づいて、1台のコンピュータ内の複数のターミナル上でEAの最適化プロセスの起動を可能にするターミナルの拡張機能を作成しました。 さらなる関連のバージョンは、新しい機能の追加を通じて、この拡張機能の可能性を拡大します。

この結果の操作は、ビデオで見ることができます。 この記事では GUI 作成プロセスの説明のみを提供し、拡張機能のロジックは次の部分で説明します。




メタトレーダー起動メソッドと設定ファイル

作成された拡張機能を詳しく検討する前に、コマンドラインを使用してターミナル(および他のアプリケーション)の起動を確認しましょう。 この方法はやや古風に見えるかもしれませんが、Linux ベースのオペレーティングシステムなどでよく使われるか、グラフィカルインターフェイスを使わずにアプリケーションを起動するためにも使用されます。

C++ で書かれたシンプルなプログラムの例に基づいて、ターミナルの起動を考えてみましょう。

#include<iostream>

using namespace std;

int main()
{
    cout<<"Hello World";

    return 0;
}

プログラムのコンパイル後、.exe ファイルが届きます。 ファイルを実行すると、通常の動作である "Hello World" メッセージがコンソールに表示されます。 開始機能 'main' にはインプットはありませんが、特殊なケースです。 別の 'main' 機能のオーバーロードを使用してこのプログラムを変更する場合は、多数のパラメータを受け取るコンソール アプリケーションを作成します。

#include<iostream>

using namespace std;

int main(int argc, char *argv[])
{
    cout << "Hello World" << endl;

    for(int i = 0; i < argc; i ++)
    {
        cout << argv[i] << endl; 
    }

    return 0;
}

最初のパラメータ 'argc' は、2 番目のパラメータの配列の長さを示します。

2番目のパラメータは、起動時に t=プログラムにインプットされた国のリストです。 このプログラムは、次のようにコンソールから呼び出すことができます。

./program "My name" "is Andrey"

ここで./program はプログラム名を示し、その他の行はスペースで区切られたパラメータです。 これらのパラメータは、渡された配列に書き込まれます。 このプログラムの実行結果を次に示します。

Hello World
./program
My name
is Andrey

最初のメッセージは前のプログラムから残り、他のすべての文字列は文字列の 'argv' 配列にパラメータとして渡されます (最初のパラメータは常に起動しているアプリケーションの名前です)。 この例は詳細には分析しませんが、MetaTrader アプリケーションをコマンド ラインから起動する方法を示しているにすぎません。

パラメータを操作する場合、通常はフラグが各フラグの前に表示されます。 C/C++ 言語には、フラグを操作するための多くの機能があります。 つまり、実行可能ファイル拡張子 (.exe) を持つシンプルなアプリケーションは、渡されたパラメータを使用してコンソールから起動でき、アプリケーションのプロパティを変更できます。  

公式instructionによると、コマンドラインを使用してMetaTraderを実行するための特別なフラグと値があります。

  • /login:login number (terminal.exe /login:100000)
  • /config:path to the configuration file (terminal.exe /config:c:\myconfiguration.ini)
  • /profile:profile name (terminal.exe /profile:Euro)
  • /portable (terminal.exe /portable)

このフラグは組み合わせることができます。 たとえば、フラグの組み合わせを使用して、指定した構成ファイルを使用して、ポータブル モードでターミナルを起動できます。

terminal.exe /config:c:\myconfiguration.ini /portable

例の「Hello World」プログラムとターミナルの差は大きいが、コマンドラインを使って起動する方法は同じです。 アドオンを開発する際には、この機能を使用します。

/configキーを使用して指定された設定ファイル、パスに特に注意してください: このファイルにより、ターミナルは起動時に使用するログイン/パスワード、およびテスター起動モードまたは一般的に実行する必要があります。 ここでは、構成ファイルの インストラクションを繰り返しません。 しかし、ファイルの構造を考えてみましょう。 各構成ファイルは、角かっこで示された一連のセクションで構成されます。

[Tester]

このセクションの後には、プログラムの開始パラメータを特徴付けたフィールドの説明を含むキー値リストが続きます。 構成ファイルには、文字で始まるコメントをインクルードすることもできます ";"または "#"です。 XAML マークアップまたは json ファイルを使用し、1 つのファイルに大量のデータを保存できる *.ini に加えて、新しい構成ファイル形式を使用できるようになりました。 しかし、メタトレーダーは*.iniファイルのみを使用します。 WinApi は、必要な形式で便利な操作を行うためのラッパ クラスを開発するときに使用した構成ファイルを使用する操作の機能をサポートしています。 MetaTrader 設定ファイルを操作するための使用する機能とラッパ 。 

希望のアドオンの機能と使用するテクノロジー

プロジェクトを操作するには、Visual Studio IDE (統合開発環境)をインストールする必要があります。 このプロジェクトは、コミュニティ 2019 バージョンを使用して作成されました。 Visual Studio のインストール中に、このアドオンの開発に使用された .Net 4.6.1 もインストールする必要があります。 C#に関する知識を持たない読者のために、特定の言語の問題やプログラミング中に使用したテクニックについて詳しく説明します。

グラフィカルインターフェイスを作成する最も便利な方法はC#言語を使用するものであり、MetaTraderターミナルはこの言語を適用するための便利な方法をサポートしています。 最近、C# を使用した GUI 作成に関連する記事がこのサイトに掲載されました。 その記事では、Win Forms テクノロジに基づく GUI 作成方法と、リフレクション メカニズムを使用してグラフィックスを起動する接続.dll について示しています。 記事作成者が使用するソリューションは十分ですが、現在の記事では、WPF テクノロジを使用して、より最新の GUI 開発方法を使用できます。 その結果、1 つの.dll 内で必要なすべてを実装しながら、接続ライブラリを回避することができました。 主なタスクを解決するには、WPF 技術を使用して記述されたグラフィック オブジェクトを格納できるプロジェクトの種類を作成する必要があります。 プロジェクトは動的ライブラリ (*.dll ファイル) にコンパイルされ、ターミナルに読み込まれます。 このプロジェクトの種類が存在します: WpfCustomControlライブラリ。 この型は、特にカスタム グラフィック オブジェクトを作成するために開発されました。 この例は、チャートをプロットするライブラリです。 このタイプは、メタトレーダーターミナルのアドオンを作成する目的など、特定の目的に使用します。 このプロジェクトの種類を作成するには、次のスクリーンショットに示すように、IDEVisual Studio のプロジェクトの一覧から選択します。

プロジェクト"オプティミゼーションマネージャエクステンション"と呼びましょう。 テーマ フォルダは、プロジェクトに最初に作成されます。 これには(*.xaml)ファイル "Generic.xaml"が含まれています: このファイルには、グラフィック オブジェクトの色、初期サイズ、インデント、および同様のプロパティを設定するスタイルが格納されます。 このファイルは後で必要になりますので、そのままにしておきましょう。 もう 1 つの自動的に生成されるファイルは、CustomControl1 クラスを含むファイルです。 このファイルは必要ありませんので、削除しましょう。 多くの記事が書かれているので、アドオンを拡大する可能性を提供する必要があります。 つまり、MVVM プログラミング テンプレートを使用する必要があります。 このパターンに慣れていない場合は、この リンクの説明をお読みください。 適切に構造化されたコードを実装するには、"View" フォルダを作成し、グラフィック ウィンドウを追加してみましょう。 グラフィック ウィンドウを作成するには、作成されたフォルダに Window (WPF) 要素を追加する必要があります (下のスクリーンショットに示すように)。


ウィンドウ ExtentionGUI.xaml と呼びましょう - これは上記のウィンドウに示されているグラフィック要素です。 名前空間について考えてみましょう。 プロジェクトを作成し、最適化マネージャイクティオンを呼び出しました。その後、Studio は自動的にメイン名前空間 "OptimisationManagerExtention"を生成します。 C# では、他の多くのプログラミング言語と同様に、名前空間はコンテナとして機能し、オブジェクトがあります。 名前空間のプロパティは、次の例で示すことができます。 

両方のクラスが同じ名前空間で宣言されているため、以下の構造は正しくありません。

namespace MyNamespace
{
    class Class_A
    {
    }

    class Class_A
    {
    }
}

両方のクラスが同じ名前にも関わらず、異なる名前空間にあるため、次のクラスの使用は許可されます。

namespace MyFirstNamespace
{
    class Class_A
    {
    }
}

namespace MySecondNamespace
{
    class Class_A
    {
    }
}

 いわゆるネストされた名前空間もあります。 これを使用すると、1 つの名前空間に他の多数の名前空間が含まれます。 この場合、次のコードも有効です。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace MyNamespace
{
    class Class_A
    {
    }

    namespace Second
    {
        class Class_A
        {
        }
    }

    namespace First
    {
        class Class_A
        {
        }
    }
}

しかし、この形式の記録は不便であるため、C# は短いレコードをサポートしており、便利です。

namespace MyNamespace
{
    class Class_A
    {
    }
}

namespace MyNamespace.First
{
    class Class_A
    {
    }
}

namespace MyNamespace.Second
{
    class Class_A
    {
    }
}

前の 2 つの例で示したコード 亜種は同じですが、2 番目の亜種の方が便利です。 ビュー フォルダを作成した後、ネストになった名前空間を作成したため、ビュー フォルダに追加されたオブジェクトが "OptimitisationManagerExtention.View" 名前空間に追加されます。 したがって、ウィンドウにもこの名前空間があります。 Generic.xaml ファイルで説明するスタイルをウィンドウ全体に適用できるようにするには、このファイルの XAML マークアップを編集する必要があります。 まず、<Style>タグで始まるコードブロックを削除する必要があります。 次に、ウィンドウの名前空間へのリンクを追加する必要があります。 これは "xmlns:local" propertyを経由します。 その結果、以下の内容を取得します。

<ResourceDictionary
    xmlns="http://scheMA.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://scheMA.microsoft.com/winfx/2006/xaml"
    xmlns:local="clr-namespace:OptimisationManagerExtention.View">

</ResourceDictionary>

ウィンドウのサイズ/色やその他のプロパティを設定するには、そのスタイルを記述する必要があります。 ここでアプリケーションの美しさについては言及しませんが、必要最小限の説明のみを記述します。 必要なデザイン、アニメーション、またはその他のフィーチャーを追加できます。 編集後、スタイルを記述するファイルが取得され、すべてのスタイルがウィンドウのすべての要素に自動的に適用されます。 とても便利ですね。

<ResourceDictionary
    xmlns="http://scheMA.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://scheMA.microsoft.com/winfx/2006/xaml"
    xmlns:local="clr-namespace:OptimisationManagerExtention.View">
    
    <!--Set the window background color-->
    <Style TargetType="{x:Type local:ExtentionGUI}">
        <Setter Property="Background" Value="WhiteSmoke"/>
    </Style>

    <!--
    Set the background color for the dividing strip, by dragging which 
    we change ranges of horizontally divided zones in the first tab 
    of our window
    -->
    <Style TargetType="GridSplitter">
        <Setter Property="Background" Value="Black"/>
    </Style>

    <!--Set the height of drop-down lists-->
    <Style TargetType="ComboBox">
        <Setter Property="Height" Value="22"/>
    </Style>

    <!--Set the height of calendars-->
    <Style TargetType="DatePicker">
        <Setter Property="Height" Value="22"/>
    </Style>

    <!--Set the height of text boxes-->
    <Style TargetType="TextBox">
        <Setter Property="Height" Value="22"/>
    </Style>

    <!--Set the height of buttons-->
    <Style TargetType="Button">
        <Setter Property="Height" Value="22"/>
    </Style>

</ResourceDictionary>

ウィンドウに適用するスタイルについては、ウィンドウの XAML マークアップでスタイルへのリンクを記述します: 開始タグの後<Window>、ウィンドウのポジションに対するリソースを使用してファイルへのパスを設定する次の構造を示します。 

<!--Connect styles-->
<Window.Resources>
    <ResourceDictionary Source="../Themes/Generic.xaml"/>
</Window.Resources>

作成された View ディレクトリに加えて、さらにディレクトリを作成します。

  • ViewExtention — ここでは、標準的な XAML マークアップの可能性を拡張するクラスを多数格納します。ビュー (グラフィックス) から ViewModel (アプリケーション ロジックの説明が格納されているグラフィックスとモデルを接続するレイヤー) にテーブル クリック イベントを渡すために使用します。
  • ViewModel — ここではビューモデルと関連オブジェクトが保存されます。

ご想像のとおり、アプリケーションのグラフィックスを担当するレイヤーは、C# 言語を直接使用せずに、XAML マークアップでのみ記述されます。 適切なディレクトリを作成した後、ウィンドウの XAML マークアップに追加する必要がある 2 つのネストされた名前空間を作成しました。 また、OptimisationManagerExtention.ViewModel名前空間に "ExtentionGUI_VM" クラスを作成してみましょう。 このクラスはコネクタ オブジェクトになります。 ただし、必要な機能を実行するには、"INotifyPropertyChanged" インターフェイスから継承する必要があります。 PropertyChanged event が含まれており、グラフィカルパーツは任意のフィールドの値の変更を通知され、グラフィックを更新する必要があります。 作成されたファイルは次のようになります。

/// <summary>
/// View Model
/// </summary>
class ExtentionGUI_VM : INotifyPropertyChanged
{
    /// <summary>
    /// The event of a change in any of the ViewModel properties 
    /// and its handlers
    /// </summary>
    #region PropertyChanged Event
    public event PropertyChangedEventHandler PropertyChanged;
    /// <summary>
    /// The PropertyChanged event handler
    /// </summary>
    /// <param name="propertyName">Updated variable name</param>
    protected virtual void OnPropertyChanged(string propertyName)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }
    #endregion
}

ウィンドウを作成してすべてのリンクを追加した後の XAML マークアップは次のようになります。

<Window x:Class="OptimisationManagerExtention.View.ExtentionGUI"
        xmlns="http://scheMA.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://scheMA.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://scheMA.microsoft.com/expression/blend/2008"
        xmlns:mc="http://scheMA.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:OptimisationManagerExtention.ViewModel"
        xmlns:viewExtention="clr-namespace:OptimisationManagerExtention.ViewExtention"
        mc:Ignorable="d"
        Title="ExtentionGUI" Height="450" Width="1100">

    <!--Connect styles-->
    <Window.Resources>
        <ResourceDictionary Source="../Themes/Generic.xaml"/>
    </Window.Resources>
    <!--Connect ViewModel-->
    <Window.DataContext>
        <local:ExtentionGUI_VM />
    </Window.DataContext>    

    <Grid>
        

    </Grid>
</Window>

ここまでで、アプリケーション用の GUI を開発するための主な準備が完了しているため、グラフィック レイヤーを作成するためのウィンドウの XAML マークアップのインプットに進むことができます。 すべてのコントロールは、<Grid/>ブロック内に書き込まれます。 XAMLマークアップの操作に十分な経験を持っていない人は、ドキュメントをチェックすることをお勧めします。 このツールに精通している人は、この記事で使用できるコード部分を使用できます。 2 つの GUI 作成方法 (WinForms/ WPF) を比較すると、明らかな差に加えて、類似点もあります。 すべてのグラフィック要素がクラス インスタンスとして表示され、抽象クラスの非表示部分 (Button や ComboBox など) に格納される WinForms インターフェイスを覚えておいてください。

したがって、WinForms グラフィカル アプリケーション全体が相互接続されたオブジェクト インスタンスのセットで構成されていることがわかります。 WPF マークアップを分析すると、同じ原理に基づいているとは考えにくいです。 たとえば、"Grid" タグの各マークアップ要素は実際にはクラスであるため、対応する名前空間のクラスのみを使用しながら、XAML マークアップを使用せずにまったく同じアプリケーションを再作成できます。 しかし、これは醜く、かさばるでしょう。 実際には、<Grid> タグを開くことによって、クラス インスタンスを作成することを示します。 次に、コンパイラ メカニズムは、指定したマークアップを解析し、必要なオブジェクトのインスタンスを作成します。 WPF アプリケーションのこのプロパティを使用すると、カスタム グラフィック オブジェクト、または標準機能を拡張するオブジェクトを作成できます。 さらに、追加機能を実装する方法について説明します。    

グラフィックス作成プロセスに関しては、<Grid/>はレイアウト ブロックであり、コントロールやその他のデザイン ブロックを配置するように設計されていることに注意してください。 ビデオからわかるように、[設定]タブと[最適化結果]タブの間で変更しても、下部(ProgressBar)は変更されません。 メイン<Grid/>ブロックを2行に分割し、その中にメインタブ (TabControll)を含むパネルが配置され、ステータスライン(ラベル)、プログレスバー、最適化開始を含むもう1つのブロック<Grid/>が配置されます。 しかし、水平方向に3つの列に分けられ、それぞれがコントロールの1つを含んでいます。(Lableプログレスバー、ボタン)

<Grid>
    <Grid.RowDefinitions>
        <RowDefinition/>
        <RowDefinition Height="27"/>
    </Grid.RowDefinitions>

    <!--Create TabControl with two tabs-->
    <TabControl>
        <!--The tab with robot settings and optimization or single test launch options-->
        <TabItem Header="Settings">
           
        </TabItem>

        <!--Tab for viewing optimization results and launching a test upon a double-click event-->
        <TabItem Header="Optimisation Result">
          
        </TabItem>
    </TabControl>

    <!--Container with a progress bar, operation status and a launch button-->
    <Grid Grid.Row="1">
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="150"/>
            <ColumnDefinition/>
            <ColumnDefinition Width="100"/>
        </Grid.ColumnDefinitions>
        <!--Status of a running operation-->
        <Label Content="{Binding Status, UpdateSourceTrigger=PropertyChanged}"/>
        <!--Progress bar-->
        <ProgressBar Grid.Column="1" 
                                     Minimum="0" 
                                     Maximum="100"
                                     Value="{Binding PB_Value, UpdateSourceTrigger=PropertyChanged}"/>
        <!--Start button-->
        <Button Margin="5,0,5,0" 
                                Grid.Column="2"
                                Content="Start"
                                Command="{Binding Start}"/>
    </Grid>
</Grid>

また、コントロールと共に使用されたプロパティについても考えてみましょう。 データを表示したり、データをインプットしたりできる各フィールドについて、その値を格納する個々のフィールドが ExtentionGUI_VM クラス (ViewMpodel オブジェクト) に作成されます。 WPF アプリケーションを作成する場合、特に MVVM パターンを使用する場合、グラフィックス要素は通常直接アクセスされないため、より便利な値渡しプロセスを使用します。 たとえば、 ProgressBarグラフィック要素の Value プロパティは、次の行で行われるデータ リンク テクノロジを使用して設定されます。

 Value="{Binding PB_Value, UpdateSourceTrigger=PropertyChanged}"

Binding プロパティの後にデータを格納するフィールドの名前が続き、プロパティUpdateSourceTriggerはグラフィック要素内のデータを更新するメソッドを示します。 PropertyChanged パラメータを指定してこのプロパティを設定することにより、この特定の要素のこの特定のプロパティを更新する必要があるのは、ExtentionGUI_VM クラスのPropertyChangedイベントがトリガーされ、リンクされた変数は、このイベントのパラメータの 1 つ、つまり "PB_Value"として渡されました。 XAML マークアップからわかるように、 ボタンにはデータリンクもありますが、ボタンのリンクはコマンド プロパティを使用して実行され、ICommand インターフェイスを介してコマンド (または ViewModel クラスで定義されたメソッド) を指します。ボタンクリックイベントで呼び出されます。 これがボタンクリックイベントと他のイベント(最適化結果テーブルをダブルクリックするなど)のリンクです。 これで、グラフィックパーツは次のようになります。


次の GUI 作成ステップは、[OptimisationResults] タブにコントロールを追加することです。 このタブには、最適化が実行されたターミナルとEAを選択するための2つのコンボボックスレックスと、更新レポートボタンが含まれています。 このタブには、2 つのネストになったタブを含むネストになった TabControlも含まれており、それぞれが最適化結果を持つテーブル(ListView)があります。 適切な XAML マークアップを次に示します。

  <!--Tab for viewing optimization results and launching a test upon a double-click event-->
            <TabItem Header="Optimisation Result">
                <Grid Margin="5">
                    <Grid.RowDefinitions>
                        <RowDefinition Height="50"/>
                        <RowDefinition/>
                    </Grid.RowDefinitions>

                    <Grid VerticalAlignment="Center">
                        <WrapPanel>
                            <Label Content="Terminal:"/>
                            <ComboBox Width="250" 
                                  ItemsSource="{Binding TerminalsAfterOptimisation}"
                                  SelectedIndex="{Binding TerminalsAfterOptimisation_Selected, UpdateSourceTrigger=PropertyChanged}"/>
                            <Label Content="Expert"/>
                            <ComboBox Width="100"  
                                  ItemsSource="{Binding BotsAfterOptimisation}"
                                  SelectedIndex="{Binding BotsAfterOptimisation_Selected, UpdateSourceTrigger=PropertyChanged}"/>
                        </WrapPanel>
                        <Button HorizontalAlignment="Right"
                            Content="Update Report"
                            Command="{Binding UpdateOptimisationReport}"/>
                    </Grid>
                    <!--Container with the optimization result tables-->
                    <TabControl 
                        TabStripPlacement="Bottom"
                        Grid.Row="1">
                        <!--A tab in which the historic optimization results are shown-->
                        <TabItem Header="Backtest">
                            <!--Table with optimization results-->

                            <ListView ItemsSource="{Binding HistoryOptimisationResults}"
                                  viewExtention:ListViewExtention.DoubleClickCommand="{Binding StartTestFromOptimisationResults}"
                                  viewExtention:ListViewExtention.DoubleClickCommandParameter="History"
                                  SelectedIndex="{Binding SelectedHistoryOptimisationRow}" >
                                <ListView.View>
                                    <GridView 
                                    viewExtention:GridViewColumns.ColumnsSource="{Binding OptimisationResultsColumnHeadders}"
                                    viewExtention:GridViewColumns.DisplayMemberMember="DisplayMember"
                                    viewExtention:GridViewColumns.HeaderTextMember="HeaderText"/>
                                </ListView.View>
                            </ListView>
                        </TabItem>
                        <!--A tab in which the results of forward optimization 
                    passes are shown-->
                        <TabItem Header="Forvard">
                            <!--Table with optimization results-->

                            <ListView ItemsSource="{Binding ForvardOptimisationResults}"
                                  viewExtention:ListViewExtention.DoubleClickCommand="{Binding StartTestFromOptimisationResults}"
                                  viewExtention:ListViewExtention.DoubleClickCommandParameter="Forvard"
                                  SelectedIndex="{Binding SelectedForvardOptimisationRow}">
                                <ListView.View>
                                    <GridView 
                                   viewExtention:GridViewColumns.ColumnsSource="{Binding OptimisationResultsColumnHeadders}"
                                   viewExtention:GridViewColumns.DisplayMemberMember="DisplayMember"
                                   viewExtention:GridViewColumns.HeaderTextMember="HeaderText"/>
                                </ListView.View>
                            </ListView>
                        </TabItem>
                    </TabControl>
                </Grid>
            </TabItem>

前述のように、XAML マークアップで使用する各タグはクラスです。 また、標準マークアップの機能を拡張する独自のクラスを作成したり、カスタムグラフィック要素を作成したりすることもできます。 現段階では、既存のマークアップの機能を拡張する必要がありました。 最適化パスの結果を持つテーブルは、異なる数の列と異なる名前を持つ必要があります: これは 最初の拡張子になります。

2 番目の拡張子は、ダブルクリックから ICommand インターフェイスへの変換です。 MVVM 開発テンプレートを使用しない場合は、ViewModel と Model をビュー レイヤーに接続してはならないため、2 番目の拡張機能を作成する必要がないようにすることができます。 これは、必要に応じてアプリケーションのグラフィック層を変更または再書き換えを可能にするために行われます。 拡張呼び出しメソッドから見られるように、すべて ViewExtention ネストされた名前空間に配置され、その後にコロンと拡張子を含むクラスの名前が続きます。 "point" 演算子の後に、値を設定するプロパティの名前が続きます。

クリック イベントを ICommand インターフェイスに変換する拡張機能から始めて、各拡張機能を考えてみましょう。 拡張処理のダブルクリック イベントを作成するには、ViewExtention フォルダに部分的なクラス ListViewExtentionを作成します。 部分アクセス修飾子は、クラスの実装が複数のファイルに分割できることを示し、すべてのメソッド/フィールドとクラスの他のコンポーネント ('partial' としてマークされていますが、2 つ以上のファイルに分割される) は同じクラスに属します。

using System.Windows;

using ICommand = System.Windows.Input.ICommand;
using ListView = System.Windows.Controls.ListView;

namespace OptimisationManagerExtention.ViewExtention
{
    /// <summary>
    /// The class of extensions for ListView, which translates events to commands (ICommand)
    /// the class is marked with keyword 'partial', i.e. its implementation is divided into several files.
    /// 
    /// In this class ListView.DoubleClickEvent is translated 
    /// into the ICommand type command
    /// </summary>
    partial class ListViewExtention
    {
        #region Command
        /// <summary>
        /// Dependent property - containing a reference to the command callback
        /// The property is set via View in the XAML markup of the project
        /// </summary>
        public static readonly DependencyProperty DoubleClickCommandProperty =
            DependencyProperty.RegisterAttached("DoubleClickCommand",
                typeof(ICommand), typeof(ListViewExtention),
                new PropertyMetadata(DoubleClickCommandPropertyCallback));

        /// <summary>
        /// Setter for DoubleClickCommandProperty
        /// </summary>
        /// <param name="obj">Control</param>
        /// <param name="value">The value to link with</param>
        public static void SetDoubleClickCommand(UIElement obj, ICommand value)
        {
            obj.SetValue(DoubleClickCommandProperty, value);
        }
        /// <summary>
        /// Getter for DoubleClickCommandProperty
        /// </summary>
        /// <param name="obj">Control</param>
        /// <returns>a link to the saved command of type ICommand</returns>

        public static ICommand GetDoubleClickCommand(UIElement obj)
        {
            return (ICommand)obj.GetValue(DoubleClickCommandProperty);
        }
        /// <summary>
        /// Callback which is called after setting property DoubleClickCommandProperty
        /// </summary>
        /// <param name="obj">Control for which the property</param>
        /// <param name="args">events preceding callback</param>
        private static void DoubleClickCommandPropertyCallback(DependencyObject obj, DependencyPropertyChangedEventArgs args)
        {
            if (obj is ListView lw)
            {
                if (args.OldValue != null)
                    lw.MouseDoubleClick -= Lw_MouseDoubleClick;

                if (args.NewValue != null)
                    lw.MouseDoubleClick += Lw_MouseDoubleClick;
            }
        }
        /// <summary>
        /// Callback of the event which is translated to the ICommand type
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private static void Lw_MouseDoubleClick(object sender, System.Windows.Input.MouseButtonEventArgs e)
        {
            if (sender is UIElement element)
            {
                object param = GetDoubleClickCommandParameter(element);
                ICommand cmd = GetDoubleClickCommand(element);
                if (cmd.CanExecute(param))
                    cmd.Execute(param);
            }
        }
        #endregion

        #region CommandParameter
        /// <summary>
        /// Dependent property - containing a reference to parameters passed to the callback of type ICommand
        /// The property is set via View in the XAML markup of the project
        /// </summary>
        public static readonly DependencyProperty DoubleClickCommandParameterProperty =
            DependencyProperty.RegisterAttached("DoubleClickCommandParameter",
                typeof(object), typeof(ListViewExtention));
        /// <summary>
        /// Setter for DoubleClickCommandParameterProperty
        /// </summary>
        /// <param name="obj">Control</param>
        /// <param name="value">The value to link with</param>
        public static void SetDoubleClickCommandParameter(UIElement obj, object value)
        {
            obj.SetValue(DoubleClickCommandParameterProperty, value);
        }
        /// <summary>
        /// Getter for DoubleClickCommandParameterProperty
        /// </summary>
        /// <param name="obj">Control</param>
        /// <returns>passed parameter</returns>
        public static object GetDoubleClickCommandParameter(UIElement obj)
        {
            return obj.GetValue(DoubleClickCommandParameterProperty);
        }
        #endregion
    }
}

WPF グラフィック オブジェクトの各クラスの各プロパティは、DependancyPropertyクラスにリンクされます。 このクラスでは、ビュー レイヤーとビューモデル レイヤー間でデータ バインディングを実行できます。 クラス インスタンスを作成するには、構成された DependencyProperty クラスを返す、指定の DependencyProperty.RegisterAttached メソッドを使用します。 このメソッドは 4 つのパラメータを受け入れます。 詳細については、こちらをご覧ください。 作成されたプロパティには、アクセス修飾子 'public static readonly' が必要です (つまり、クラスの外部からアクセス可能で、クラス インスタンスを作成しなくてもこのプロパティを呼び出す可能性があり、'static' 修飾子は、このプロパティの unity を設定します) に注意してください。この特定のアプリケーション内で'読み取り専用'は、プロパティを変更不可にします。.

  1. 最初のパラメータは、プロパティが XAML マークアップに表示される名前を設定します。
  2. 2 番目のパラメータは、バインディングを実行する要素の型を設定します。 この型のオブジェクトは、DependancyProperty クラスの作成されたインスタンスに格納されます。 
  3. 3 番目のパラメータは、プロパティが配置されているクラスの型を設定します。 この例では、クラスは ListViewExtention です。
  4. 直近のパラメータは PropertyMetadata クラス インスタンスを受け入れます - このパラメータは、DependancyProperty クラス インスタンスの作成後に呼び出されるイベントのハンドラを参照します。 このコールバックは、ダブルクリック イベントをサブスクライブするために必要です。

このプロパティから値を正しく設定して取得できるようにするために、DependancyProperty クラス インスタンスの作成中に渡された名前とプレフィックスセット (値を設定または取得する)で構成されるメソッドを作成します。 どちらのメソッドも静的である必要があります。 基本的に、既存のメソッドSetValueGetValueの使用をカプセル化します。

依存プロパティの作成の完了に関連するイベントのコールバックは、テーブル行をダブルクリックし、以前にサブスクライブされたイベント (存在する場合) のサブスクライブ解除のイベントへのサブスクリプションを実装します。 ダブルクリック イベントハンドラ内では、ビューに渡された ICommand からCanExecute メソッドと Executeメソッドが順番に呼び出されます。 よって、サブスクライブされたテーブルの行のいずれかをダブルクリックした場合、このイベントの発生後に実行されるロジックのメソッドの呼び出しを含むイベント ハンドラを自動的に呼び出します。

この作成されたクラスは、実際には中間クラスです。 ViewModel からイベントと呼び出しメソッドを処理しますが、ビジネス ロジックは実行しません。 このアプローチは、ダブルクリックイベントハンドラ(WinFormsで実装されているように)からメソッドを直接呼び出すよりも混乱しているように見えるかもしれませんが、このアプローチを使用する理由があります: ビューが何も知らないべきではないと述べている MVVM パターンを観察する必要があります。その逆もしかりです。

中間クラスを使用することで、前述のプログラミング パターンを使用するクラス間の接続性を減らします。 これで、ViewModel クラスを編集できます。 ただし、中間クラスがアクセスする特定の ICommand 型プロパティを 1 つ指定する必要があります。

拡張機能には、SelectionChanged イベントを ICommand に変換するプロパティの実装と、コレクションを格納するバインドされたファイルに基づいてテーブルの列を自動的に作成する中間クラスも含まれています。 2 つの XAML マークアップ拡張機能は前述のように実装されているため、詳細については説明しません。 質問がある場合は、この記事へのコメントで尋ねてください。 [最適化結果] タブのマークアップを実装したので、ウィンドウは次のようになります。


次のステップでは、[設定] タブを実装します。 便宜上、このタブに XAML マークアップのフルバージョンを追加するのではなく、基本的なグラフィック オブジェクトを説明する部分をここに示します。 完全なコードは以下に添付されています。

<!--The tab with robot settings and optimization or single test launch options-->
            <TabItem Header="Settings">
                <!--Container with settings and other items-->
                <Grid>
                    <Grid.RowDefinitions>
                        <RowDefinition/>
                        <RowDefinition Height="200"/>
                    </Grid.RowDefinitions>

                    <!--Container with the list of selected terminals-->
                    <Grid>
                        <Grid.RowDefinitions>
                            <RowDefinition Height="30"/>
                            <RowDefinition/>
                        </Grid.RowDefinitions>
                        <!--Container with the selection of terminals which are determined automatically-->
                        <WrapPanel HorizontalAlignment="Right" 
                                       VerticalAlignment="Center">
                            <!--List with terminals-->
                            <ComboBox Width="200" 
                                          ItemsSource="{Binding TerminalsID}"
                                          SelectedIndex="{Binding SelectedTerminal, UpdateSourceTrigger=PropertyChanged}"
                                          IsEnabled="{Binding IsTerminalsLVEnabled, UpdateSourceTrigger=PropertyChanged}"/>
                            <!--Terminal adding button-->
                            <Button Content="Add" Margin="5,0"
                                    Command="{Binding AddTerminal}"
                                    IsEnabled="{Binding IsTerminalsLVEnabled, UpdateSourceTrigger=PropertyChanged}"/>
                        </WrapPanel>
                        <!--List of selected terminals-->
                        <ListView Grid.Row="1"
                                  ItemsSource="{Binding SelectedTerminalsForOptimisation}"
                                  SelectedIndex="{Binding SelectedTerminalIndex, UpdateSourceTrigger=PropertyChanged}"
                                  IsEnabled="{Binding IsTerminalsLVEnabled, UpdateSourceTrigger=PropertyChanged}" >
                            <ListView.View>
                                <GridView>
                                .
                                .
                                .
                                </GridView>
                            </ListView.View>
                        </ListView>
                    </Grid>
                    <!--Container with parameters for editing and 
                    optimization settings-->
                    <TabControl
                                Grid.Row="2" 
                                Margin="0,0,0,5"
                                TabStripPlacement="Right">
                        <!--Robot parameters tab-->
                        <TabItem Header="Bot params" >
                            <!--List with robot parameters-->
                            <ListView 
                                    ItemsSource="{Binding BotParams, UpdateSourceTrigger=PropertyChanged}">
                                <ListView.View>
                                    <GridView>
                                    .
                                    .
                                    .
                                    </GridView>
                                </ListView.View>
                            </ListView>
                        </TabItem>
                        <!--Optimization settings tab-->
                        <TabItem Header="Settings">
                            <Grid MinWidth="700"
                                          MinHeight="170"
                                          MaxWidth="750"
                                          MaxHeight="170">
                                <Grid.ColumnDefinitions>
                                    <ColumnDefinition/>
                                    <ColumnDefinition/>
                                    <ColumnDefinition/>
                                </Grid.ColumnDefinitions>
                                <Grid.RowDefinitions>
                                    <RowDefinition/>
                                    <RowDefinition/>
                                    <RowDefinition/>
                                </Grid.RowDefinitions>
                                <!--Login seen by the robot-->
                                <StackPanel 
                                            Margin="2"
                                            VerticalAlignment="Center">
                                    <Label Content="Login:"/>
                                    <TextBox Text="{Binding TestLogin, UpdateSourceTrigger=PropertyChanged}"/>
                                </StackPanel>
                                <!--Execution type-->
                                <StackPanel 
                                            Margin="2"
                                            VerticalAlignment="Center"
                                            Grid.Column="1"
                                            Grid.Row="1">
                                    <Label Content="Execution:"/>
                                    <ComboBox 
                                            DataContext="{Binding ExecutionList}"
                                            ItemsSource="{Binding ItemSource}"
                                            SelectedIndex="{Binding SelectedIndex, UpdateSourceTrigger=PropertyChanged}"/>
                                </StackPanel>
                                <!--Type of history passing for tests-->
                                <StackPanel 
                                            Margin="2"
                                            VerticalAlignment="Center"
                                            Grid.Column="2"
                                            Grid.Row="1">
                                    <Label Content="Model:"/>
                                    <ComboBox 
                                            DataContext="{Binding ModelList}"
                                            ItemsSource="{Binding ItemSource}"
                                            SelectedIndex="{Binding SelectedIndex, UpdateSourceTrigger=PropertyChanged}"/>
                                </StackPanel>
                                <!--Optimization criteria-->
                                <StackPanel 
                                            Margin="2"
                                            VerticalAlignment="Center"
                                            Grid.Column="2"
                                            Grid.Row="2">
                                    <Label Content="Optimisation criteria:"/>
                                    <ComboBox DataContext="{Binding OptimisationCriteriaList}"
                                                  ItemsSource="{Binding ItemSource}"
                                                  SelectedIndex="{Binding SelectedIndex, UpdateSourceTrigger=PropertyChanged}"/>
                                </StackPanel>
                                <!--Forward period start date-->
                                <StackPanel 
                                            Margin="2"
                                            VerticalAlignment="Center"
                                            Grid.Column="1"
                                            Grid.Row="0">
                                    <Label Content="Forward date:"/>
                                    <DatePicker SelectedDate="{Binding ForvardDate, UpdateSourceTrigger=PropertyChanged}"/>
                                </StackPanel>
                                <!--Deposit-->
                                <StackPanel 
                                            Margin="2"
                                            VerticalAlignment="Center"
                                            Grid.Column="0"
                                            Grid.Row="1">
                                    <Label Content="Deposit:"/>
                                    <ComboBox DataContext="{Binding Deposit}" 
                                                  ItemsSource="{Binding ItemSource}"
                                                  SelectedIndex="{Binding SelectedIndex, UpdateSourceTrigger=PropertyChanged}"/>
                                </StackPanel>
                                <!--Profit calculation currency-->
                                <StackPanel 
                                            Margin="2"
                                            VerticalAlignment="Center"
                                            Grid.Column="0"
                                            Grid.Row="2">
                                    <Label Content="Currency:"/>
                                    <ComboBox DataContext="{Binding CurrencyList}"
                                                  ItemsSource="{Binding ItemSource}"
                                                  SelectedIndex="{Binding SelectedIndex, UpdateSourceTrigger=PropertyChanged}"/>
                                </StackPanel>
                                <!--Leverage-->
                                <StackPanel 
                                            Margin="2"
                                            VerticalAlignment="Center"
                                            Grid.Column="1"
                                            Grid.Row="2">
                                    <Label Content="Leverage:"/>
                                    <ComboBox DataContext="{Binding LaverageList}"
                                                  ItemsSource="{Binding ItemSource}"
                                                  SelectedIndex="{Binding SelectedIndex, UpdateSourceTrigger=PropertyChanged}"/>
                                </StackPanel>
                                <!--Whether to use test visualizer-->
                                <CheckBox Content="Visual mode"
                                              Margin="2"
                                              VerticalAlignment="Center"
                                              Grid.Column="2"
                                              Grid.Row="0"
                                              IsChecked="{Binding IsVisual, UpdateSourceTrigger=PropertyChanged}"/>
                            </Grid>
                        </TabItem>
                    </TabControl>

                    <!--Separator line which allows resizing 
                    one area relative to the other one-->
                    <GridSplitter Height="3" VerticalAlignment="Bottom" HorizontalAlignment="Stretch"/>

                </Grid>
            </TabItem>

まず、動的に編集可能な領域の実装について考えてみましょう。 このフォームの動作は、メイン <Grid/>に 2 行を形成し、<GridSplitter/>要素を追加することによって実装されます。 ドラッグして、ターミナルのリストと他のテーブルの領域のサイズを変更します。 生成されたテーブルの最初の行に、新しい<Grid/>を挿入します。 最初の部分には、ターミナルのリストと新しいターミナルを追加するためのボタンを含む、もう 1 つのレイアウト要素である WrapPanelがあります。 2番目の部分には、追加されたターミナルのリストを含むテーブルがあります。

テキストに加えて、テーブル内のデータを変更できるコントロールもテーブルに含まれています。 テーブルに値を変更/追加するためのデータ バインディング テクノロジのおかげで、テーブルはコントロール データのコレクションに直接関連付けられているため、追加のコードを記述する必要はありません。 編集可能な<Grid/>ブロックの下部には、 テスター設定ロボットパラメータのリストを含むテーブル含まれているTabControlがあります。

したがって、この拡張機能のグラフィカルシェルの生成を行いました。 ViewModel の説明に進む前に、テーブルバインドメソッドを考えてみましょう。

ロボット パラメータを持つテーブルの例に基づく説明を次に示します。

  • Flag — パラメータを最適化する必要がある場合
  • パラメータ名
  • テスターで使用するパラメータの値
  • パラメータ列挙開始
  • パラメータの列挙終了
  • パラメータ列挙ステップ

パラメータをすべてテーブルに渡すには、テーブル行データを格納するストレージ クラスを作成する必要があります。 つまり、このクラスはすべてのテーブル列を記述する必要があり、クラスのコレクションにはテーブル全体が格納されます。 テーブルに対して次のクラスが作成されました。

/// <summary>
/// The class describing rows for the table with the robot parameter settings before optimization
/// </summary>
class ParamsItem
{
    /// <summary>
    /// Class constructor
    /// </summary>
    /// <param name="Name">The name of the variable</param>
    public ParamsItem(string Name) => Variable = Name;
    /// <summary>
    /// The flag showing whether this robot variable needs to be optimized
    /// </summary>
    public bool IsOptimize { get; set; }
    /// <summary>
    /// Variable name
    /// </summary>
    public string Variable { get; }
    /// <summary>
    /// The value of the variable selected for the test
    /// </summary>
    public string Value { get; set; }
    /// <summary>
    /// Parameters enumeration start
    /// </summary>
    public string Start { get; set; }
    /// <summary>
    /// Parameters enumeration step
    /// </summary>
    public string Step { get; set; }
    /// <summary>
    /// Parameters enumeration end
    /// </summary>
    public string Stop { get; set; }
}

このクラスの各 property には、特定の列に関連する情報があります。 次に、データ コンテキストがどのように変更されるかを見てみましょう。 アプリケーション ウィンドウを作成する際に、ウィンドウのデータ ソースが、このウィンドウのメイン DataContext であり、テーブルが関連付けられているコレクションを含むクラス ExtentionGUI_VM であることを最初に示しました。 ただし、この特定のテーブルの特定の行ごとに DataContext はクラス ExtentionGUI_VM から ParamsItem に変更されます。 これは、重要なポイントなので、プログラム コードからこのテーブルのセルを更新する必要がある場合は、ExtentionGUI_VM クラスではなく、この特定の行のコンテキスト クラスで PropertyChanged イベントを呼び出す必要があります。

したがって、これでグラフィカルレイヤー作成プロセスの説明を完了し、アプリケーションとプログラムロジックを接続するクラスの説明に進むことができます。


ビューモデルとメタトレーダーと実装されたdllの間のコネクタ

プログラムの次のコンポーネントは、上記の説明のグラフィックスとロジックの接続を担当する部分です。 使用するプログラミング テンプレート (モデル ビュー ビュー モデルまたは MVVM) では、この部分は ViewModel と呼ばれ、適切な名前空間 (最適化ManagerExtention.ViewModel) にあります。

この記事の最初の章では、既に ExtentionGUI_VM クラスを作成し、INotifyPropertyChanged インターフェイスを実装しました 。 View からのデータがリンクされている ExtentionGUI_VM クラスのすべてのフィールドは、変数としてではなく、プロパティとして宣言する必要があります。 この C# 言語構造に慣れていない場合は、以下のコードと説明を参照してください。

class A
{
    /// <summary>
    /// This is a simple public field to which you can set values or read values from it 
    /// But there is no possibility to perform a check or other actions.
    /// </summary>
    public int MyField = 5;
    /// <summary>
    /// This property allows processing data before reading or writing
    /// </summary>
    public int MyGetSetProperty
    {
        get
        {
            MyField++;
            return MyField;
        }
        set
        {
            MyField = value;
        }
    }

    // This is a read-only property
    public int GetOnlyProperty => MyField;
    /// <summary>
    // This is a write-only property
    /// </summary>
    public int SetOnlyProperty
    {
        set
        {
            if (value != MyField)
                MyField = value;
        }
    }
}

この例からわかるように、プロパティはメソッドとフィールドのハイブリッドの一種です。 値を返すか、記録されたデータを検証する前に、特定のアクションを実行できます。 また、プロパティは 読み取り専用または書き込み専用です。 データ バインディングを実装したときに、View で C# コンストラクトを参照しました。

ExtentionGUI_VM クラスを実装するときは、ブロック (#endregion#region構成) に分割しました。 View では、最適化結果の作成から始め、このタブを作成するためのプロパティとメソッドについて考えてみましょう。 便宜上、まずこのタブに表示されるデータを担当するコードを提供し、その後説明を追加します。

#region Optimisation Result

/// <summary>
/// Table with historical optimization results
/// </summary>
public DataTable HistoryOptimisationResults => model.HistoryOptimisationResults;
/// <summary>
/// Table with forward optimization results
/// </summary>
public DataTable ForvardOptimisationResults => model.ForvardOptimisationResults;
/// <summary>
/// Observable collection with a list of optimization columns
/// </summary>
public ObservableCollection<ColumnDescriptor> OptimisationResultsColumnHeadders =>
       model.OptimisationResultsColumnHeadders;

#region Start test from optimisation results
/// <summary>
/// Run the test for the selected optimization process
/// </summary>
public ICommand StartTestFromOptimisationResults { get; }
/// <summary>
/// The method that starts a test upon a double-click
/// </summary>
/// <param name="type"></param>
private void StartTestFromOptimisationResultsAction(object type)
{
    ENUM_TableType tableType = (string)type == "History" ?
        ENUM_TableType.History : ENUM_TableType.Forvard;
    int ind = tableType == ENUM_TableType.History ?
        SelectedHistoryOptimisationRow : SelectedForvardOptimisationRow;

    model.StartTest(tableType, ind);
}
#endregion

/// <summary>
/// Index of the selected row from the historical optimization table
/// </summary>
public int SelectedHistoryOptimisationRow { get; set; } = 0;
/// <summary>
/// Index of the selected row from the forward optimization
/// </summary>
public int SelectedForvardOptimisationRow { get; set; } = 0;

#region UpdateOptimisationReport

#region TerminalsAfterOptimisation
public ObservableCollection<string> TerminalsAfterOptimisation => model.TerminalsAfterOptimisation;
public int TerminalsAfterOptimisation_Selected
{
    get => model.TerminalsAfterOptimisation_Selected;
    set
    {
        model.TerminalsAfterOptimisation_Selected.SetVarSilently(value);
        if (value > -1)
           model.SelectNewBotsAfterOptimisation_forNewTerminal();
    }
}
        
public ObservableCollection<string> BotsAfterOptimisation => model.BotsAfterOptimisation;
public int BotsAfterOptimisation_Selected
{
    get => model.BotsAfterOptimisation_Selected;
    set => model.BotsAfterOptimisation_Selected.SetVarSilently(value);
}
#endregion
public ICommand UpdateOptimisationReport { get; }

        private void UpdateReportsData(object o)
        {
            model.LoadOptimisations();
        }
        #endregion
        #endregion

historicalおよびforward最適化テーブルのデータ ソースと、中間クラス (GridViewColumns) を介して 2 つのテーブルの列に接続されている 列のリストを考えてみましょう。 各テーブルには、データ ソース (DataTable による型) と、テーブル内の選択した行のインデックスを含むプロパティの 2 つの一意のフィールドがあります。 選択したテーブル行のインデックスは表示では重要ではありませんが、テーブル行をダブルクリックしてテストを実行する場合など、さらなるアクションに必要になります。 テーブルへのデータの読み込みとそのデータのクリアは、プログラム ロジックによって実装されます。OOP の原則に従って、1 つの特定のクラスが 1 つの特定のタスクを担当する必要があり、テーブルコンポジションに関するデータを提供するプロパティでは、モデルのメイン クラス (ExtemtionGUI_M) の対応するプロパティを参照するだけです。 選択したインデックスの追跡は、テーブル フィールドをマウスでクリックして自動的に実行されるため、プロパティはアクションやチェックを実行しません。 これは、クラス フィールドに似ています。

また、列のリスト (最適化結果列ヘッドダー) - 観察可能なコレクション<T>を含むプロパティの使用済みデータ型にも注意してください。 動的に変更可能なコレクションを格納する標準の C# クラスの 1 つです。 ただし、リスト (List<T>) とは異なり、このクラスには CollectionChanged イベントが含まれており、コレクション内のデータが変更/削除/追加されるたびに呼び出されます。 このクラスによってプロパティ型を作成すると、データ ソースの変更に関するビューの自動通知が表示されます。 したがって、表示されたデータを書き直す必要性をグラフィックスに手動で通知する必要がなくなります。 

次に、ターミナルとロボットの選択を使用してドロップダウンリストに注意を払うだけでなく、ボタン押下とテーブルクリックイベントハンドラの実装に進みます。 ドロップダウン リストと最適化結果の読み込みのブロックは、#region UpdateOptimisationReport としてマークされた領域に含まれています。 まず、ターミナルのリストを含む最初のドロップダウン リストのデータ ソースを検討します。 これが、最適化が実行されたターミナル ID の一覧と、選択したターミナルのインデックスです。 ターミナルのリストはモデルによってコンパイルされるため、モデル内の適切なフィールドを参照するだけです。 選択したターミナルインデックスの選択は、もう少し複雑なタスクです。 前述のフィールドに対するプロパティの利点を使用してみましょう。 ドロップダウン リストからターミナルを選択すると、 TerminalsAfterOptimisation_Selectedプロパティセッターが呼び出され、次のアクションが実行されます。

  1. モデル内の選択したインデックスの保存
  2. このターミナルで最適化されたロボットのリストを格納する 2 番目のドロップダウン リストの値を更新します。

拡張機能は、実行されたテストのヒストリーを格納し、ロボットとターミナルでグループ化します。 同じターミナルで同じロボットを再最適化すると、過去のヒストリーが書き換えられます。 ビューからビューモデルにイベントを渡すこの方法は、最も便利な方法です。 しかし、これは常に適切とは言えません。

グラフィックスレイヤーから ViewModel にイベントを渡す次の方法は、コマンドの使用です。 Button サポート コマンドなどの一部のグラフィック要素。 コマンドを使用する場合、'command' プロパティを、パラメータ化された ICommand 型で ViewModel のプロパティにリンクします。 ICommand インターフェイスは C# 言語の標準インターフェイスの 1 つであり、次のようになります。

public interface ICommand
{
    //
    // Summary:
    //     Occurs when changes occur that affect whether or not the command should execute.
    event EventHandler CanExecuteChanged;
 
    //
    // Summary:
    //     Defines the method that determines whether the command can execute in its current
    //     state.
    //
    // Parameters:
    //   parameter:
    //     Data used by the command. If the command does not require data to be passed,
    //     this object can be set to null.
    //
    // Returns:
    //     true if this command can be executed; otherwise, false.
    bool CanExecute(object parameter);
    //
    // Summary:

    //     Defines the method to be called when the command is invoked.
    //
    // Parameters:
    //   parameter:
    //     Data used by the command. If the command does not require data to be passed,
    //     this object can be set to null.
    void Execute(object parameter);
}

ボタンをクリックすると、ConExecute イベントが最初にトリガーされ、false を返すとボタンにアクセスできなくなります。 この機能を使用するには、このインターフェイスを実装する必要があります。 インターフェイスを実装するときに何も新しいものを作らず、その標準的な実装を使用しました。

/// <summary>
/// Implementation of the ICommand interface, used for
/// binding commands with methods from ViewModel
/// </summary>
class RelayCommand : ICommand
{
    #region Fields 
    /// <summary>
    /// Delegate directly performing the action
    /// </summary>
    readonly Action<object> _execute;
    /// <summary>
    /// Delegate checking for the possibility of performing an action
    /// </summary>
    readonly Predicate<object> _canExecute;
    #endregion // Fields

    /// <summary>
    /// Constructor
    /// </summary>
    /// <param name="execute">The method passed for the delegate, which is a callback</param>
    public RelayCommand(Action<object> execute) : this(execute, null) { }
    /// <summary>
    /// Constructor
    /// </summary>
    /// <param name="execute">
    /// The method passed for the delegate, which is a callback
    /// </param>
    /// <param name="canExecute">
    /// The method passed for the delegate, which checks the possibilities to perform an action
    /// </param>
    public RelayCommand(Action<object> execute, Predicate<object> canExecute)
    {
        if (execute == null)
            throw new ArgumentNullException("execute");
        _execute = execute; _canExecute = canExecute;
    }

    /// <summary>
    /// Checking the possibility to perform an action
    /// </summary>
    /// <param name="parameter">parameter passed from View</param>
    /// <returns></returns>
    public bool CanExecute(object parameter)
    {
        return _canExecute == null ? true : _canExecute(parameter);
    }
    /// <summary>
    /// Event - called whenever the callback execution ability changes.
    /// When this event is triggered, the form calls the "CanExecute" method again
    /// The event is triggered from ViewModel when needed
    /// </summary>
    public event EventHandler CanExecuteChanged
    {
        add { CommandManager.RequerySuggested += value; }
        remove { CommandManager.RequerySuggested -= value; }
    }
    /// <summary>
    /// The method calling a delegate which performs the action
    /// </summary>
    /// <param name="parameter">parameter passed from View</param>
    public void Execute(object parameter) { _execute(parameter); }
}

この ICommand インターフェイスの実装によると、2 つのプライベート読み取り専用フィールドが作成され、デリゲートが格納され、Relaycommandクラスコンストラクタの 1 つを介して渡されたストア メソッドが格納されます。 このメカニズムを使用するには、RelayCommand クラス インスタンスを ExtentionGUI_VM クラス コンストラクタに作成します。 このインスタンスに、アクションを実行するメソッドを渡します。 最適化テーブルの情報を更新する UpdateOptimisationReportプロパティは、次のようになります。

UpdateOptimisationReport = new RelayCommand(UpdateReportsData);

ここではUpdateReportsDataは、ExtentionGUI_M クラス (モデル クラスから) から LoadOptims() メソッドを呼び出す ExtentionGUI_VM クラスのプライベート メソッドです。 同様に、 StartTestFromOptimisationResultsResultsプロパティは、ユーザーが選択したテーブル行をダブルクリックしたイベントとリンクされます。 ただし、この場合、ダブルクリック イベントは標準プロパティ (ボタンクラスなど) ではなく、前述の実装済みのソリューション "ListViewExtention.DoubleClickCommand" を介して渡されます。 Execute メソッドシグネチャと CanExecute メソッドシグネチャから見られるように、'オブジェクト' 型の値を受け入れることができます。 ボタンの場合、値は渡されません。ダブルクリック イベントの場合、テーブル名を渡します: XAML マークアップでプロパティを持つバインディング メソッドから見ることができます。    

viewExtention:ListViewExtention.DoubleClickCommand="{Binding StartTestFromOptimisationResults}"
viewExtention:ListViewExtention.DoubleClickCommandParameter="History"

このパラメータに基づいて、最適化テストパスを実行するためにデータを取るべきテーブルをモデルを理解できます。

次に、メイン コントロールが配置されている [設定] タブで操作するためのプロパティとコールバックの実装について考えてみましょう。 まず、選択したターミナルを含むテーブルのデータ ソースの実装から始めましょう。

#region SelectedTerminalsForOptimisation && SelectedTerminalIndex (first LV params)
/// <summary>
/// The list of terminals selected for optimization, which is displayed in the terminals table
/// </summary>
public ObservableCollection<TerminalAndBotItem> SelectedTerminalsForOptimisation { get; private set; } =
    new ObservableCollection<TerminalAndBotItem>();
/// <summary>
/// The index of the selected row
/// </summary>
private int selectedTerminalIndex = 0;
public int SelectedTerminalIndex
{
    get { return selectedTerminalIndex; }
    set
    {
        // Assign the value of the newly selected index
        selectedTerminalIndex = value;

        //((RelayCommand)Start).OnCanExecuteChanged();

        // Fill in the list of parameters of the robot selected in the current row
        if (value == -1)
        {
            return;
        }
        TerminalAndBotItem terminal_item = SelectedTerminalsForOptimisation[value];
        if (terminal_item.Experts.Count > 0)
        {
            FillInBotParams(terminal_item.Experts[terminal_item.SelectedExpert],
                terminal_item.TerminalID);
        }
    }
}
        #endregion

このターミナルのリストは、TerminalAndBotItem クラスによって型指定された観測コレクションとして表示されます。 コレクションは ViewModel クラスに格納されます。 ViewModel には、選択した行の インデックスを設定および取得するためのプロパティも含まれています。 ビデオに示すように、行をクリックすると、選択したロボット パラメータが動的に読み込まれます。 この動作は、 SelectedTerminalIndexプロパティ セッターに実装されます。

また、選択したターミナルを持つテーブル内の行にはコントロールが含まれているため、TerminalAndBotItemをデータ コンテキスト クラスとして編成する必要があることも覚えておいてください。

まず、ターミナルの一覧からターミナルを削除します。 前述のように、テーブルのデータは ViewModel に格納されますが、テーブル内の削除ボタンのコールバックは行データのコンテキスト、つまり、このコレクションにアクセスできないTerminalAndBotItemクラスのコンテキストにのみバインドできます。 この場合の解決策は、デリゲートを使用することです。 ExtentionGUI_VM にデータ削除メソッドを実装し、コンストラクタを介して TerminalAndBotItem クラスに渡しました。 わかりやすくするために、以下のコードですべての余分な行を削除しました。 外部から自分自身を削除するメソッドの受け渡しは、 次のようになります。

class TerminalAndBotItem
{
    
    public TerminalAndBotItem(List<string> botList,
        string TerminalID,
        Action<string, string> FillInBotParams,
        Action<TerminalAndBotItem> DeleteCommand)
    {
        // Fill in the delegate fields
        #region Delegates
        this.FillInBotParams = FillInBotParams;
        this.DeleteCommand = new RelayCommand((object o) => DeleteCommand(this));
        #endregion
    }

    #region Delegates
    /// <summary>
    /// Field with the delegate to update selected robot parameters
    /// </summary>
    private readonly Action<string, string> FillInBotParams;
    /// <summary>
    /// Callback for a command to delete a terminal from the list (Delete button in the table)
    /// </summary>
    public ICommand DeleteCommand { get; }
    #endregion

    /// <summary>
    /// index of the selected EA
    /// </summary>
    private int selectedExpert;
    /// <summary>
    /// Property for the index of the selected EA
    /// </summary>
    public int SelectedExpert
    {
        get { return selectedExpert; }
        set
        {
            selectedExpert = value;
            // Run the callback to load parameters for the selected EA 
            if (Experts.Count > 0)
                FillInBotParams(Experts[selectedExpert], TerminalID);
        }
    }
}

このフラグメントから分かるように、このタスクの実装では別の C# 言語構造が使用されました: ラムダ式です。 C++ または C# に精通している場合、このコード部分に何ら変な点は見当たりません。 ラムダ式は同じ機能と見なすことができますが、主な差は、従来の宣言を持っていないということです。 これらの構造は、C #で広く使用されており、 ここでそれについて読むことができます。 コールバックは ICommand を使用して実行されます。 クラス実装の次の興味深い点は、すべてのロボットのドロップダウンリストから新しいロボットを選択する際のロボットパラメータの更新です。 ロボット パラメータを更新するメソッドはモデル内にあり、ViewModel のこのメソッド ラッパの実装は ViewModel 内にあります (ターミナルの削除方法もあります)。 ここでも、デリゲートを使用しますが、ICommand を使用する代わりに、 SelectedExpertプロパティ セッターに新しいロボット選択イベントへの応答を配置します。

EAパラメータを更新するメソッドには、特定の機能、つまり非同期機能もあります。

private readonly object botParams_locker = new object();
/// <summary>
/// Get and fill robot parameters
/// </summary>
/// <param name="fullExpertName"> FullEAname in relation to folder ~/Experts</param>
/// <param name="Terminal">ID of the terminal</param>
private async void FillInBotParams(string fullExpertName, string Terminal)
{
    await System.Threading.Tasks.Task.Run(() =>
    {
        lock (botParams_locker)
        {
            model.LoadBotParams(fullExpertName, Terminal, out OptimisationInputData? optimisationData);
            if (!optimisationData.HasValue)
                return;

            IsSaveInModel = false;
            TestLogin = optimisationData.Value.Login;
            IsVisual = optimisationData.Value.IsVisual;
            ForvardDate = optimisationData.Value.ForvardDate;
            CurrencyList.SelectedIndex = optimisationData.Value.CurrencyIndex;
            Deposit.SelectedIndex = optimisationData.Value.DepositIndex;
            ExecutionList.SelectedIndex = optimisationData.Value.ExecutionDelayIndex;
            LaverageList.SelectedIndex = optimisationData.Value.LaverageIndex;
            ModelList.SelectedIndex = optimisationData.Value.ModelIndex;
            OptimisationCriteriaList.SelectedIndex = optimisationData.Value.OptimisationCriteriaIndex;
            IsSaveInModel = true;
        }
    });


    OnPropertyChanged("BotParams");
}

C# には、書き込みしやすい非同期プログラミング モデルである Async Await があり、今回使用しました。 提示されたコード スニペットは、非同期操作を開始し、実装の完了を待ちます。 操作が完了すると、Onproperty 変更されたイベントが呼び出され、テーブル内の変更のビューにロボット パラメータのリストが表示されます。 特定の機能を理解するために、Async Await テクノロジーを使用した非同期アプリケーションの例を考えてみましょう。 

class Program
{
    static void Main(string[] args)
    {
        Console.WriteLine($"Main before Method() = {Thread.CurrentThread.ManagedThreadId}");
        Method();
        Console.WriteLine($"Main after Method() = {Thread.CurrentThread.ManagedThreadId}");

        Console.ReadLine();
    }
    private static async void Method()
    {
        Console.WriteLine($"Before Await = {Thread.CurrentThread.ManagedThreadId}");
        await Task.Run(() => { Thread.Sleep(100); Console.WriteLine($"In Avait 1 = {Thread.CurrentThread.ManagedThreadId}"); });
        Console.WriteLine($"After Await 1 = {Thread.CurrentThread.ManagedThreadId}");
      Thread.Sleep(100);

            await Task.Run(() => { Console.WriteLine($"In Avait 2 = {Thread.CurrentThread.ManagedThreadId}"); });
            Console.WriteLine($"After Await 2 = {Thread.CurrentThread.ManagedThreadId}");
        }

    }

このシンプルなコンソール アプリケーションの目的は、スレッドの動作を示し、非同期について説明することです。 Main メソッドでは、最初にMain メソッドが実行されているスレッドの IDを表示し、次に非同期メソッドを開始し、Mainスレッドの ID を再び表示します。 非同期メソッドでは、 このメソッドが実行されているスレッドの IDを再度表示し、非同期スレッドの IDと非同期スレッドの後に操作が実行されるスレッドの ID を 1 つずつPrintします。このプログラムの最も興味深い出力です:

Main before Method() = 1

Before Await = 1

Main After Method() = 1

In Await 1 = 3

After Await 1 = 3

In Await 2 = 4

After Await 2 = 4

上記から分かるように、メインスレッドと非同期Method() からの最初の出力は同じで、同じ ID です。 これは、Method() が完全に非同期ではないことを意味します。 このメソッドの非同期は、static Task.Run() メソッドを使用した非同期操作の呼び出しの後に開始されます。 Method() が完全に同期している場合、メイン スレッドの ID を示す 次のイベントは、次の 4 つのメッセージの出力後に呼び出されます。 

次に、非同期出力を見てみましょう。 最初の非同期出力は、予想される ID = 3 を返します。 ただし、次の操作は非同期操作の完了を待機し ('await' を使用して) し、ID = 3 も返します。 2 番目の非同期操作では、同じ図が観察されます。 また、最初の非同期操作の後に使用されたスレッド ID の出力後に追加された 100 ミリ秒の遅延にも関わらず、2 番目の操作は最初のスレッドとは別のスレッドで開始されますが、オーダーは変更されません。

Async Await モデルと一般的な非同期の特定の機能です。 セカンダリ スレッドが 2 回呼び出される可能性がある場合、メソッド内のすべてのアクションがコンテキストで実行され、エラーが発生する可能性があります。 このために、lock(locker_object){} コンストラクトが使用します。 この設計では、例と同様に呼び出し実行キューのようなものが作成されます。 C# メカニズムを介してキューが独立して形成されるテスト例とは対照的に、ここではスイッチとして機能する共有リソースを使用します。 lock() コンストラクトで使用されている場合、他のメソッド呼び出しは、インプットされるまで共有リソース ステージでスタックします。 したがって、二重メソッド呼び出しエラーを回避します。

次に、オプティマイザ パラメータ設定用のデータ ソースの作成について考えてみましょう。 このコードを次に示します。

#region Optimization and Test settings

/// <summary>
/// The login visible to the robot during tests (it is required if there is limitation by login)
/// </summary>
private uint? _tertLogin;
public uint? TestLogin
{
    get => _tertLogin;
    set
    {
        _tertLogin = value;

        OnPropertyChanged("TestLogin");
        CB_Action(GetSetActionType.Set_Index);
    }
}
/// <summary>
/// Order execution delay
/// </summary>
public ComboBoxItems<string> ExecutionList { get; }
/// <summary>
/// Type of used quotes (every tick, OHLC, 1M ...)
/// </summary>
public ComboBoxItems<string> ModelList { get; }
/// <summary>
/// Optimization criterion
/// </summary>
public ComboBoxItems<string> OptimisationCriteriaList { get; }
/// <summary>
/// Deposits
/// </summary>
public ComboBoxItems<int> Deposit { get; }
/// <summary>
/// Profit calculation currency
/// </summary>
public ComboBoxItems<string> CurrencyList { get; }
/// <summary>
/// Leverage
/// </summary>
public ComboBoxItems<string> LaverageList { get; }
/// <summary>
/// Forward test start date
/// </summary>
private DateTime _DTForvard = DateTime.Now;
public DateTime ForvardDate
{
    get => _DTForvard;
    set
    {
        _DTForvard = value;

        OnPropertyChanged("ForvardDate");
        CB_Action(GetSetActionType.Set_Index);
    }
}
/// <summary>
/// Indication of tester start in the graphical mode
/// </summary>
private bool _isVisualMode = false;
/// <summary>
/// Indication of tester start in the visual mode
/// </summary>
public bool IsVisual
{
    get => _isVisualMode;
    set
    {
        _isVisualMode = value;

        OnPropertyChanged("IsVisual");
        CB_Action(GetSetActionType.Set_Index);
    }
}
/// <summary>
/// a hidden variable which stores the IsSaveInModel flag value
/// </summary>
private bool isSaveInModel = true;
/// <summary>
/// Shared resource for asynchronous access to the IsSaveInModel property
/// </summary>
private readonly object SaveModel_locker = new object();
/// <summary>
/// Flag; if True - if tester parameters are changed, they will be saved
/// </summary>
private bool IsSaveInModel
{
    get
    {
        lock (SaveModel_locker)
            return isSaveInModel;
    }
    set
    {
        lock (SaveModel_locker)
            isSaveInModel = value;
    }
}
/// <summary>
/// Callback saving changes in tester parameters
/// </summary>
/// <param name="actionType"></param>
private void CB_Action(GetSetActionType actionType)
{
    if (actionType == GetSetActionType.Set_Index && IsSaveInModel)
    {
        model.UpdateTerminalOptimisationsParams(new OptimisationInputData
        {
            Login = TestLogin,
            IsVisual = IsVisual,
            ForvardDate = ForvardDate,
            CurrencyIndex = CurrencyList.SelectedIndex,
            DepositIndex = Deposit.SelectedIndex,
            ExecutionDelayIndex = ExecutionList.SelectedIndex,
            LaverageIndex = LaverageList.SelectedIndex,
            ModelIndex = ModelList.SelectedIndex,
            OptimisationCriteriaIndex = OptimisationCriteriaList.SelectedIndex,
            Deposit = Deposit.ItemSource[Deposit.SelectedIndex],
            Currency = CurrencyList.ItemSource[CurrencyList.SelectedIndex],
            Laverage = LaverageList.ItemSource[LaverageList.SelectedIndex]
        });
    }
}
#endregion

もう一つの重要な点は、オプティマイザパラメータの実装です。 このモデルでは、各ロボットに対してテスター設定の個々のインスタンスが格納されます。 これより、選択した各ターミナルの個々のテスター構成が可能になります。 適切な CB_Action メソッドは各セッターで呼び出されるため、パラメータの変更時にモデル内の結果を即座に保存できます。 また、ドロップダウン リストのデータを格納する ComboBoxItems<T> クラスも作成しました。 実際には、接続されているコンボボックスのコンテキストです。 クラスの実装を次に引き出します。

/// <summary>
/// Class - a wrapper for ComboBox list data
/// </summary>
/// <typeparam name="T">Data type stored in ComboBox</typeparam>
class ComboBoxItems<T> : INotifyPropertyChanged
{
    /// <summary>
    /// Collection of list items
    /// </summary>
    private List<T> items;
    public List<T> ItemSource
    {
        get
        {
            OnAction(GetSetActionType.Get_Value);
            return items;
        }
        set
        {
            items = value;
            OnAction(GetSetActionType.Set_Value);
        }
    }
    /// <summary>
    /// Selected index in the list
    /// </summary>
    int selectedIndex = 0;
    public int SelectedIndex
    {
        get
        {
            OnAction(GetSetActionType.Get_Index);
            return selectedIndex;
        }
        set
        {
            selectedIndex = value;
            OnAction(GetSetActionType.Set_Index);
        }
    }

    public event Action<GetSetActionType> Action;
    public event PropertyChangedEventHandler PropertyChanged;

    private void OnAction(GetSetActionType type)
    {
        switch (type)
        {
            case GetSetActionType.Set_Value:
                PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("ItemSource"));
                break;
            case GetSetActionType.Set_Index:
                PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("SelectedIndex"));
                break;
        }
        Action?.Invoke(type);
    }
}
enum GetSetActionType
{
    Get_Value,
    Set_Value,
    Get_Index,
    Set_Index
}

 その特定の機能は、イベントの 1 つが編集されるか、イベントでデータが受信されるたびに呼び出されるイベントです。 もう 1 つの機能は、 プロパティの変更に関するビューの自動更新です。したがって、ビューモデルとビューの両方にプロパティの変更を通知できます。 したがって、ViewModel では、オプティマイザ設定の変更されたプロパティに関して、モデル内のデータを更新し、自動保存を呼び出します。 また、各コンボボックスの 2 つのプロパティ (選択した要素のインデックスとすべての要素のリスト) を ViewModel に追加するため、コードが読みやすくなります。 このクラスがないと、ExtentionGUI_VM クラス コードはさらに大きくなります。  

結論として、アドオンのモデルをインスタンス化する方法と、MetaTrader5ターミナルでGUIを実行する方法を見てみましょう。 データ モデル クラスは ViewModel から独立している必要があり、ビューモデルはビューから独立している必要があります。 これで、テストの可能性に、IExtentionGUI_Mインターフェイスを介してモデルを実装します。 このインターフェイスの構造と実装は、データ モデルの説明と共に考慮されます。 ここで、ExtentionGUI_VM クラスはデータ モデルの特定の実装を知らないことに注意してください - 代わりに IExtentionGUI_M インターフェイスで動作し、モデル クラスは次の方法でインスタンス化されます。

private readonly IExtentionGUI_M model = ModelCreator.Model;

このインスタンス化プロセスでは、静的ファクトリを使用します。 ModelCreator クラスはファクトリであり、次のように実装されます。

/// <summary>
/// Factory for substituting a model in a graphical interface
/// </summary>
class ModelCreator
{
    /// <summary>
    /// Model
    /// </summary>
    private static IExtentionGUI_M testModel;
    /// <summary>
    /// Property returning either a model (if it has not been substitutes) or a substitutes model (for tests)
    /// </summary>
    internal static IExtentionGUI_M Model => testModel ?? new ExtentionGUI_M(new MainTerminalCreator(),
                                                                             new MainConfigCreator(),
                                                                             new MainReportReaderCreator(),
                                                                             new MainSetFileManagerCreator(),
                                                                             new OptimisationExtentionWorkingDirectory("OptimisationManagerExtention"),
                                                                             new MainOptimisatorSettingsManagerCreator(),
                                                                             new TerminalDirectory(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "MetaQuotes", "Terminal")));

    /// <summary>
    /// Model substitution method substitutes a test model so that you can test the graphics separately from the logic
    /// </summary>
    /// <param name="model">test model - substituted from the outside</param>
    [System.Diagnostics.Conditional("DEBUG")]
    public static void SetModel(IExtentionGUI_M model)
    {
        testModel = model;
    }
}

このクラスには、データ モデル インターフェイスによって型を型化したプライベート フィールドがあります。 このフィールドは、最初は null と等しくなります。 リクエストされたモデルを受け取った静的プロパティを作成するときに、この機能を使用しました。 上記のコードでチェックが実行されます: testModel は、ビジネス ロジックを含むモデルの実装をインスタンス化し、NULL です。 testModelが null と等しくない場合 (モデルを置き換えました)、代わりのモデルを返します。 静的メソッドSetModelは、モデルの置換に使用します。 このメソッド ID は、 このプログラムのリリース バージョンでの使用を禁止する [System.Diagnostics.Conditional("DEBUG")] 属性によって装飾されています。

GUI の起動プロセスは、前述の記事で説明した.dll からグラフィックスを実行するのと似ています。 パブリック MQLConnector クラスは、MetaTrader との接続を実装するために作成されました。 

/// <summary>
/// Class for connecting the graphical interface with MetaTrader
/// </summary>
public class MQL5Connector
{
    /// <summary>
    /// Field containing a pointer to a running graphical interface
    /// </summary>
    private static View.ExtentionGUI instance;
    /// <summary>
    /// Method that launches the graphical interface. 
    /// Only one interface is launched from one robot. 
    /// During launch a check is performed if the GUI has already been started. 
    /// If yes, the new one is not started
    /// </summary>
    /// <param name="pathToTerminal">Path to the terminal's mutable folder</param>
    public static void Instance(string terminalID)
    {
        // check if the GUI has already been started
        if (instance == null)
        {
            // Variable of the secondary thread - the GUI thread (graphics are launched in the secondary thread)
            // Its instantiation and passing a lambda expression describing the order of graphics start
            Thread t = new Thread(() =>
            {
                // Instantiation of the GUI class and its display (launch of graphics)
                instance = new View.ExtentionGUI();
                instance.Show();
                // Subscribe to the graphics window closing event - if the window is closed then 
                // the field in which the link to the HUI was stored is assigned the null value
                instance.Closed += (object o, EventArgs e) => { instance = null; };

                // Launch GUI thread dispatcher
                Dispatcher.Run();
            });
            MainTerminalID = terminalID;		

            // Start secondary thread
            t.SetApartmentState(System.Threading.ApartmentState.STA);
            t.Start();
        }
    }     
    /// <summary>
    /// Gets data on whether the window is active
    /// </summary>
    /// <returns>true if active and false if closed</returns>
    public static bool IsWindowActive() => instance != null;
    /// <summary>
    /// Main Terminal ID
    /// </summary>
    internal static string MainTerminalID { get; private set; }
    internal static Dispatcher CurrentDispatcher => ((instance == null) ? Dispatcher.CurrentDispatcher : instance.Dispatcher);
}

このクラスはパブリックアクセス修飾子でマークする必要があります - MetaTraderのロボットからアクセスできるようになります。 また、ターミナルで使用するメソッドは静的で、パブリック アクセス修飾子を持つ必要があります。 このクラスには、内部アクセス修飾子を持つ 2 つのプロパティもあります。 このアクセス修飾子は、作成された.dll 内でのみ使用することを目的としているため、ターミナルから非表示にします。 実装からわかるように、ウィンドウは プライベート静的フィールドに格納されることになっています。 したがって、他のプロパティやメソッドからアクセスできます。 また、このソリューションでは、このターミナル上の 1 つのロボットで作成できるアプリケーション インスタンスが 1 つだけになります。 このInstance メソッドは、グラフィックスをインスタンス化し、ウィンドウを開きます。 最初に、ウィンドウが以前にインスタンス化されたかどうかの チェックが実行されます。 「はい」の場合は、試行は無視する必要があります。 次に、グラフィックスを実行するためのセカンダリ スレッドが作成されます。 グラフィックスと実行中のプログラムのスレッドの分離は、ターミナルとグラフィカル インターフェイスのフリーズを回避するために使用します。 ウィンドウの読み込みを書き込んだ後、ウィンドウの閉じるイベントを サブスクライブし、ウィンドウ読み込みスキームの適切な操作に null 値を割り当てます。 その後、ディスパッチャーを起動する必要があります。 Dispatcher クラスは、WPF アプリケーションのマルチスレッドの問題を解決するために作成されました。 実際には、グラフィックス ウィンドウのすべての要素がグラフィックス ウィンドウ スレッドに属します。 グラフィックス要素の値を別のスレッドから変更しようとすると、"クロススレッド例外' エラーが表示されます。 Dispatcher クラスは、グラフィック インターフェイス スレッドのデリゲートを介して渡された操作を開始し、エラーを回避します。 グラフィックス起動用のラムダ式の説明を完了したら、 スレッドをシングル スレッド アパートメントとして構成して実行し、グラフィックスを実行する必要があります。 先立って、渡された現在のターミナル ID の値を格納する必要があります。

なぜ必要なのでしょうか。 これより、ロジックとは別にグラフィックスをデバッグできます。 グラフィカルインターフェイスを作成しました。 ただし、デバッグするには、モデルを表すクラスが必要です。 このモデルは、特定の実装機能の数があるため、グラフィックスとは別にデバッグする必要があります。 テスト データ モデルを置き換えるメソッドができたので、テスト データ モデル クラスを実装し、静的ファクトリを使用して ViewModel に置き換えることができます。 その結果、テスト データを使用してグラフィックスをデバッグし、GUI を実行し、コールバック、デザイン、その他のニュアンスの反応を確認する機会が得られます。 次のようにしました。 まず、VisualStudio からグラフィックスを直接実行するには、現在のソリューションでコンソール アプリケーションを作成する必要があります。


これを"Test" と呼び、MetaTrader 用に書き込む.dll へのリンクを追加します。 その結果、dll のパブリック クラスを使用できるコンソール アプリケーションが得られます。 ただし、dll にはパブリック クラスが 1 つしかありません。 これを行うには、dll 内でのみ使用可能なクラスにアクセスする必要があります。 その解決策があります。 これを行うには、dll の任意の場所で次の属性を示します。

[assembly: InternalsVisibleTo("Test")]

dll のすべての内部クラスをテスト ビルド (テスト コンソール アプリケーション) で使用できるようにします。 したがって、フェイクのモデルを作成し、アプリケーションを起動するために使用することができます。 その結果、コンソール アプリケーションには次の実装が必要です。

 class Program
 {
    static void Main(string[] args)
    {
        ModelCreator.SetModel(new MyTestModel());

        MQL5Connector.Instance("ID of the main terminal");
    }
}

class MyTestModel : IExtentionGUI_M
{
    // Implementation of the IExtentionGUI_M interface
}

これで、ロジックとは別にグラフィックスを実行し、デバッグして視覚的に分析できます。

結論と添付ファイル

グラフィック アプリケーション 層とそのコネクタ クラス (ViewModel) を作成するうえで最も重要で興味深い点を調べました。 この段階では、開いてクリックできるグラフィックスを実装し、グラフィックレイヤーのデータソースとその動作(ボタンを押す反応など)を記述するリンククラスを作成しました。 さらに今後は、アドオンのロジックとファイル、ターミナル、およびコンピュータ ディレクトリとのデータのやり取り方法について説明するモデル クラスとそのコンポーネントについて説明します。

MetaQuotes Ltdによってロシア語から翻訳されました。
元の記事: https://www.mql5.com/ru/articles/7029

添付されたファイル |
MetaTraderプログラムを簡単かつ迅速に開発するためのライブラリ(第10部): MQL4との互換性 - ポジションオープンイベントと指値注文発動イベント MetaTraderプログラムを簡単かつ迅速に開発するためのライブラリ(第10部): MQL4との互換性 - ポジションオープンイベントと指値注文発動イベント
前の記事では、MetaTrader 5とMetaTrader 4プラットフォーム用のプログラムの開発を単純化するための大規模なクロスプラットフォームライブラリの作成を始めました。第9部では、MQL4を使用するためのライブラリクラスの改善を開始しました。ここでは、MQL4との完全な互換性を確保するために、ライブラリの改善を続けます。
MetaTraderプログラムを簡単かつ迅速に開発するためのライブラリ(第9部): MQL4との互換性 - データの準備 MetaTraderプログラムを簡単かつ迅速に開発するためのライブラリ(第9部): MQL4との互換性 - データの準備
前の記事では、MetaTrader 5とMetaTrader 4プラットフォーム用のプログラムの開発を単純化するための大規模なクロスプラットフォームライブラリの作成を始めました。第8部では、注文とポジションの変更イベントを追跡するためのクラスを実装しました。ここでは、MQL4と完全な互換性を備えさせることでライブラリを改善します。
MetaTraderプログラムを簡単かつ迅速に開発するためのライブラリ(第4部)MQL4との互換性 - ポジション決済イベント MetaTraderプログラムを簡単かつ迅速に開発するためのライブラリ(第4部)MQL4との互換性 - ポジション決済イベント
MetaTrader 5およびMetaTrader 4プラットフォーム用のプログラムの開発を簡素化する大規模なクロスプラットフォームライブラリの開発を継続します。第10部では、MQL4とのライブラリの互換性に関する作業を再開し、ポジションを開くイベントと未決注文の発動イベントを定義しました。本稿では、ポジション決済イベントを定義し、未使用の注文プロパティを取り除きます。
Google サービスによるメーリング キャンペーンの手配 Google サービスによるメーリング キャンペーンの手配
トレーダーは、他のトレーダー、クライアントや友人とのビジネス関係を維持するために、メーリングキャンペーンを手配したい場合があるかもしれません。 その場合、スクリーンショット、ログ、またはレポートを送信する必要がある場合があります。 頻繁に発生するタスクではないかもしれませんが、このような機能があれば明らかに利点となります。 この記事では、複数の Google サービスを同時に使用し、C# で適切なアセンブリを開発し、MQL ツールと統合を取り上げています。