1.USBデバッグアシスタントのデモ
このデモでは、MVVM 設計パターンに基づいた WPF フレームワークを使用して USB デバッグ アシスタントを実装します。その効果を図に示します。
実現機能:上位コンピュータ(USBデバッグアシスタント)が下位コンピュータ(ZYNQ)と通信し、USB経由でデータの送受信を行う 実験環境:
Visual Studio 2022
コントロールライブラリ:HandyControlは便利なオープンソースのWPFコントロールを使用 HandyControl
USBライブラリ:LibUsbDotNet
プロジェクト完了:一緒に便利に使用 WPF MVVM は USB デバッグ アシスタントを作成します デモの完全なプロジェクト ファイル
Gitee リンク: https://gitee.com/haileifly/wpf-usb.git
2. 機能デモ
デモ コードの構造は図に示されており、MVVM 設計パターンを参照することで完成します。
プロジェクトが実行されると、インターフェイスは次の図のようになります。
USB デバイスは 2 つの ID を介して接続されています。デフォルトの ID はランダムに入力されるため、直接クリックすると、连接USB设备
図に示すように、デバイスが見つからないというポップアップが表示されます。
下のコンピュータが正常に接続されたら、図に示すように、ドロップダウン ボックスで対応する USB デバイスを選択できます。
図に示すように、送受信されるバイト数も設定できます。
設定が完了したら、クリックするだけで连接USB设备
、図に示すように、Bus Hound スーパー ソフトウェア バス プロトコル アナライザーを開いて、送受信データを観察できます。
データ入力後USB发送数据区
、クリックして送信を開始すると、バスハウンドが送信されたことがわかります 図のように、送信データ形式がこのようになるのは、通信プロトコルのヘッダフレーム、チェックフレーム、トレーラが構成されているためです。上部のコンピューターと下部のコンピューターはカスタマイズされています。フレーム:
をクリックする开始接收
と、下のコンピュータが上のコンピュータ インターフェイスにデータを返し、Bus Hound が受信したデータを表示します。図に示すように、この 2 つは一致しています。
データを再度送受信します。
最後に、断开USB设备
図に示すように、 をクリックします。
3. UIインターフェイスXAML
UI インターフェイス コード MainWindow.xaml は次のとおりです。
<Window
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:Wpf_USB"
xmlns:hc="https://handyorg.github.io/handycontrol" x:Class="Wpf_USB.MainWindow"
mc:Ignorable="d"
Title="WPF_USB" Height="480" Width="800" ResizeMode="CanMinimize">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="160*"/>
<ColumnDefinition Width="160*"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="60*"/>
<RowDefinition Height="60*"/>
<RowDefinition Height="60*"/>
<RowDefinition Height="60*"/>
</Grid.RowDefinitions>
<GroupBox Header="USB设备信息" BorderBrush="Black" FontSize="20" FontWeight="Bold" Margin="5,5,5,5" Grid.RowSpan="2">
<StackPanel>
<StackPanel Orientation="Horizontal">
<TextBlock TextWrapping="Wrap" Text="VendorID:" Width="auto" RenderTransformOrigin="0.478,1.336" Padding="0,10,0,0" Height="44" Margin="10"/>
<ComboBox Width="177" Margin="15,10,5,10" ItemsSource="{Binding UsbPar.VendorID}" SelectedItem="{Binding CuPar.VendorID}" IsEnabled="{Binding CuPar.EnableSelect}"/>
</StackPanel>
<StackPanel Orientation="Horizontal">
<TextBlock TextWrapping="Wrap" Text="ProductID:" Width="auto" RenderTransformOrigin="0.478,1.336" Padding="0,10,0,0" Height="44" Margin="10"/>
<ComboBox Width="177" Margin="10,10,5,10" ItemsSource="{Binding UsbPar.ProductID}" SelectedItem="{Binding CuPar.ProductID}" IsEnabled="{Binding CuPar.EnableSelect}"/>
</StackPanel>
<StackPanel Orientation="Horizontal">
<Button Content="连接USB设备" Margin="20,0,10,0" FontSize="16" Height="38" Command="{Binding OpenUsbDev}"/>
<Button Content="断开USB设备" Margin="20,0,10,0" FontSize="16" Height="38" Command="{Binding CloseUsbDev}"/>
</StackPanel>
</StackPanel>
</GroupBox>
<GroupBox Header="USB接收数据区" FontSize="20" FontWeight="Bold" BorderBrush="#FF7ED866" Margin="5,5,5,5" Grid.Row="2" Grid.RowSpan="2">
<ScrollViewer VerticalScrollBarVisibility="Auto">
<TextBox Name="ReceiveData" TextWrapping="Wrap" Text="{Binding CuPar.ReadDataString}" FontWeight="Normal" FontSize="16"/>
</ScrollViewer>
</GroupBox>
<GroupBox Header="接收数据设置" FontSize="20" FontWeight="Bold" Margin="5,5,5,5" BorderBrush="#FF5570C7" Grid.Column="1">
<StackPanel Orientation="Horizontal">
<TextBlock HorizontalAlignment="Left" TextWrapping="Wrap" Text="BytesRead:" Padding="0,15,0,0"/>
<ComboBox Width="130" Height="40" ItemsSource="{Binding UsbPar.BytesRead}" SelectedItem="{Binding CuPar.BytesRead}" IsEnabled="{Binding CuPar.EnableSelect}"/>
<Button Content="开始接收" FontSize="20" Height="58" Margin="5" Command="{Binding UsbDevRead}"/>
</StackPanel>
</GroupBox>
<GroupBox Header="发送数据设置" FontSize="20" FontWeight="Bold" Margin="5,5,5,5" BorderBrush="#FF5D29A0" Grid.Column="1" Grid.Row="1">
<StackPanel Orientation="Horizontal">
<TextBlock HorizontalAlignment="Left" TextWrapping="Wrap" Text="BytesWrite:" Padding="0,15,0,0"/>
<ComboBox Width="130" Height="40" ItemsSource="{Binding UsbPar.BytesWrite}" SelectedItem="{Binding CuPar.BytesWrite}" IsEnabled="{Binding CuPar.EnableSelect}"/>
<Button Content="开始发送" FontSize="20" Height="58" Margin="5" Command="{Binding UsbDevWrite}"/>
</StackPanel>
</GroupBox>
<GroupBox Header="USB发送数据区" FontSize="20" FontWeight="Bold" Margin="5,5,5,5" BorderBrush="#FFA42D84" Grid.Column="1" Grid.RowSpan="2" Grid.Row="2">
<ScrollViewer VerticalScrollBarVisibility="Auto">
<TextBox Name="SendData" TextWrapping="Wrap" Text="{Binding CuPar.WriteData}" FontWeight="Normal" FontSize="16"/>
</ScrollViewer>
</GroupBox>
</Grid>
</Window>
オープンソースのコントロールライブラリHandyControlを使用するため、App.xamlのコードは次のように記述する必要があります。
<Application x:Class="Wpf_USB.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:Wpf_USB"
StartupUri="MainWindow.xaml">
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="pack://application:,,,/HandyControl;component/Themes/SkinDefault.xaml" />
<ResourceDictionary Source="pack://application:,,,/HandyControl;component/Themes/Theme.xaml" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Application.Resources>
</Application>
4. バックグラウンドロジックCS
バックグラウンドの MainWindow.xaml.cs コードは次のとおりです。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using Wpf_USB.ViewModels;
namespace Wpf_USB
{
/// <summary>
/// MainWindow.xaml 的交互逻辑
/// </summary>
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
this.DataContext = new MainWindowViewModel();
}
}
}
USB パラメータ初期化クラス、つまりインターフェイス初期化パラメータの UsbParameter.cs コードは次のとおりです。
using LibUsbDotNet.Main;
using LibUsbDotNet;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
namespace Wpf_USB.ViewModels
{
class UsbParameter : NotificationObject
{
private ObservableCollection<int> vendorid = new ObservableCollection<int>() {
1234 };
public ObservableCollection<int> VendorID
{
get {
return vendorid; }
set
{
vendorid = value;
this.RaisePropertyChanged("VendorID");
}
}
private ObservableCollection<int> productid = new ObservableCollection<int>() {
1 };
public ObservableCollection<int> ProductID
{
get {
return productid; }
set
{
productid = value;
this.RaisePropertyChanged("ProductID");
}
}
private ObservableCollection<int> bytesread = new ObservableCollection<int>() {
512, 1024, 2048, 4096, 8192, 16384 };
public ObservableCollection<int> BytesRead
{
get {
return bytesread; }
set
{
bytesread = value;
this.RaisePropertyChanged("BytesRead");
}
}
private ObservableCollection<int> byteswrite = new ObservableCollection<int>() {
512, 1024, 2048, 4096, 8192, 16384 };
public ObservableCollection<int> BytesWrite
{
get {
return byteswrite; }
set
{
byteswrite = value;
this.RaisePropertyChanged("BytesWrite");
}
}
public void UsbDeviceFind()
{
UsbRegDeviceList allDevices = UsbDevice.AllDevices;
if (allDevices != null)
{
foreach (UsbRegistry usbRegistry in allDevices)
{
VendorID.Add(usbRegistry.Vid);
ProductID.Add(usbRegistry.Pid);
}
UsbDevice.Exit();
}
else {
MessageBox.Show("UsbDevive Not Found."); }
}
}
}
USB 更新データ クラス、CurrentParameter.cs コードは次のとおりです。
using LibUsbDotNet;
using LibUsbDotNet.Main;
using System;
using System.Collections.Generic;
using System.IO.Ports;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Input;
using System.Windows;
using System.Runtime.Remoting.Messaging;
using System.Collections.ObjectModel;
using System.Reflection;
using Wpf_USB.Models;
namespace Wpf_USB.ViewModels
{
class CurrentParameter : NotificationObject
{
public CurrentParameter()
{
GloVar = new GlobalVariable();
DataPro = new DataProcess();
}
private GlobalVariable gloVar;
public GlobalVariable GloVar
{
get {
return gloVar; }
set
{
gloVar = value;
}
}
private DataProcess dataPro;
public DataProcess DataPro
{
get {
return dataPro; }
set
{
dataPro = value;
}
}
private int vendorid;
public int VendorID
{
get {
return vendorid; }
set
{
vendorid = value;
this.RaisePropertyChanged("VendorID");
}
}
private int productid;
public int ProductID
{
get {
return productid; }
set
{
productid = value;
this.RaisePropertyChanged("ProductID");
}
}
private int bytesread;
public int BytesRead
{
get {
return bytesread; }
set
{
bytesread = value;
this.RaisePropertyChanged("BytesRead");
}
}
private int byteswrite;
public int BytesWrite
{
get {
return byteswrite; }
set
{
byteswrite = value;
this.RaisePropertyChanged("BytesWrite");
}
}
private UsbDevice usbDevice;
private UsbEndpointReader epReader;
private UsbEndpointWriter epWriter;
public UsbDevice UsbDevice
{
get {
return usbDevice; }
set {
usbDevice = value; this.RaisePropertyChanged("UsbDevice"); }
}
private byte[] readData;
private string writeData;
private string readDataString;
private bool isOpen;
private bool enableSelect = true;
public bool EnableSelect
{
get {
return enableSelect; }
set {
enableSelect = value; this.RaisePropertyChanged("EnableSelect"); }
}
public byte[] ReadData
{
get {
return readData; }
set {
readData = value; this.RaisePropertyChanged("ReadData"); }
}
public string ReadDataString
{
get {
return readDataString; }
set {
readDataString = value; this.RaisePropertyChanged("ReadDataString"); }
}
public string WriteData
{
get {
return writeData; }
set {
writeData = value; this.RaisePropertyChanged("WriteData"); }
}
public bool IsOpen
{
get {
return isOpen; }
set {
isOpen = value; this.RaisePropertyChanged("IsOpen"); }
}
public bool UsbDevicesOpen()
{
if (usbDevice != null && usbDevice.IsOpen)
{
return UsbDevicesClose();
}
try
{
UsbDeviceFinder UsbFinder = new UsbDeviceFinder(this.VendorID, this.ProductID);
usbDevice = UsbDevice.OpenUsbDevice(UsbFinder);
if (usbDevice == null) throw new Exception("Device Not Found.");
IUsbDevice wholeUsbDevice = usbDevice as IUsbDevice;
if (!ReferenceEquals(wholeUsbDevice, null))
{
wholeUsbDevice.SetConfiguration(1);
wholeUsbDevice.ClaimInterface(0);
}
epReader = usbDevice.OpenEndpointReader(ReadEndpointID.Ep01);
epWriter = usbDevice.OpenEndpointWriter(WriteEndpointID.Ep01);
EnableSelect = false;
if (usbDevice.IsOpen)
{
return IsOpen = true;
}
else
{
return IsOpen = false;
}
}
catch (Exception ex)
{
MessageBox.Show(ex.Message);
}
return IsOpen = false;
}
public bool UsbDevicesClose()
{
try
{
EnableSelect = true;
if (usbDevice == null) throw new Exception("Device Not Found.");
else
{
if (usbDevice.IsOpen)
{
IUsbDevice wholeUsbDevice = usbDevice as IUsbDevice;
if (!ReferenceEquals(wholeUsbDevice, null))
{
// Release interface #0.
wholeUsbDevice.ReleaseInterface(0);
}
usbDevice.Close();
return IsOpen = false;
}
usbDevice = null;
UsbDevice.Exit();
return IsOpen = false;
}
}
catch (Exception ex)
{
MessageBox.Show(ex.Message);
return IsOpen = false;
}
}
public void UsbDevicesRead()
{
try
{
if (usbDevice == null) throw new Exception("Device Not Found.");
else
{
int bytesReadCount = BytesRead;
readData = new byte[bytesReadCount];
ErrorCode ec = epReader.Read(ReadData, 2000, out bytesReadCount);
this.DataPro.ReadDataCRC(ReadData);
ReadDataString += GloVar.CMD_H.ToString("X2") + " ";
ReadDataString += GloVar.CMD_L.ToString("X2") + " ";
ReadDataString += GloVar.RESULT_F.ToString("X2") + " ";
for (int i = 0; i < GloVar.ReadValidData.Length; i++)
ReadDataString += GloVar.ReadValidData[i].ToString("X2") + " ";
if (bytesReadCount == 0) MessageBox.Show("No More Bytes.");
}
}
catch (Exception ex)
{
MessageBox.Show(ex.Message);
}
}
public void UsbDevicesWrite()
{
try
{
if (usbDevice == null) throw new Exception("Device Not Found.");
else
{
if (!String.IsNullOrEmpty(WriteData))
{
int bytesWriteCount = BytesWrite;
DataPro.WriteDataCRC(WriteData);
ErrorCode ec = epWriter.Write(GloVar.WriteValidData, 2000, out bytesWriteCount);
if (ec != ErrorCode.None) MessageBox.Show("Write Fail.");
}
else
{
MessageBox.Show("WriteData Is Empty.");
}
}
}
catch (Exception ex)
{
MessageBox.Show(ex.Message);
}
}
}
}
応答データのリアルタイム更新クラス、NotificationObject.cs コードは次のとおりです。
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Wpf_USB.ViewModels
{
class NotificationObject : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
public void RaisePropertyChanged(string propertyName)
{
if (this.PropertyChanged != null)
{
this.PropertyChanged.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
}
}
ボタン コマンド クラス、DelegateCommand.cs コードは次のとおりです。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Input;
namespace Wpf_USB.ViewModels
{
class DelegateCommand : ICommand
{
public bool CanExecute(object parameter)
{
if (CanExecuteFunc == null)
return true;
return this.CanExecuteFunc(parameter);
}
public event EventHandler CanExecuteChanged;
public void Execute(object parameter)
{
if (ExecuteAction == null)
{
return;
}
this.ExecuteAction(parameter);
}
public Action<object> ExecuteAction {
get; set; }
public Func<object, bool> CanExecuteFunc {
get; set; }
}
}
MainWindowViewModel.cs コードは次のとおりです。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Wpf_USB.ViewModels
{
class MainWindowViewModel : NotificationObject
{
public MainWindowViewModel()
{
UsbPar = new UsbParameter();
CuPar = new CurrentParameter();
this.UsbPar.UsbDeviceFind();
CuPar.VendorID = UsbPar.VendorID[0];
CuPar.ProductID = UsbPar.ProductID[0];
CuPar.BytesRead = UsbPar.BytesRead[3];
CuPar.BytesWrite = UsbPar.BytesWrite[3];
this.OpenUsbDev = new DelegateCommand();
this.OpenUsbDev.ExecuteAction = new Action<object>(this.OpenUsb);
this.CloseUsbDev = new DelegateCommand();
this.CloseUsbDev.ExecuteAction = new Action<object>(this.CloseUsb);
this.UsbDevRead = new DelegateCommand();
this.UsbDevRead.ExecuteAction = new Action<object>(this.UsbRead);
this.UsbDevWrite = new DelegateCommand();
this.UsbDevWrite.ExecuteAction = new Action<object>(this.UsbWrite);
}
private UsbParameter usbPar;
public UsbParameter UsbPar
{
get {
return usbPar; }
set
{
usbPar = value;
this.RaisePropertyChanged("UsbPar");
}
}
private CurrentParameter cuPar;
public CurrentParameter CuPar
{
get {
return cuPar; }
set
{
cuPar = value;
this.RaisePropertyChanged("CuPar");
}
}
public DelegateCommand OpenUsbDev {
get; set; }
private void OpenUsb(object parameter)
{
this.CuPar.UsbDevicesOpen();
}
public DelegateCommand CloseUsbDev {
get; set; }
private void CloseUsb(object parameter)
{
this.CuPar.UsbDevicesClose();
}
public DelegateCommand UsbDevRead {
get; set; }
private void UsbRead(object parameter)
{
this.CuPar.UsbDevicesRead();
}
public DelegateCommand UsbDevWrite {
get; set; }
private void UsbWrite(object parameter)
{
this.CuPar.UsbDevicesWrite();
}
}
}
カスタム通信プロトコルを含むデータ処理クラス、DataProcess.cs コードは次のとおりです。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using Wpf_USB.ViewModels;
namespace Wpf_USB.Models
{
class DataProcess : NotificationObject
{
public DataProcess()
{
GloVar = new GlobalVariable();
}
private GlobalVariable gloVar;
public GlobalVariable GloVar
{
get {
return gloVar; }
set
{
gloVar = value;
this.RaisePropertyChanged("GloVar");
}
}
public void ReadDataCRC(byte[] ReadData)
{
int CheckoutSum = 0;
int DataLength = ReadData[2] * 256 + ReadData[3];
if (ReadData[0].ToString("X") != "7E" || ReadData[1].ToString("X") != "7E")
{
GloVar.FrameData_F = true;
MessageBox.Show("Data Frame Header Error.");
}
for (int i=0; i< DataLength; i++)
{
CheckoutSum += ReadData[i + 4];
}
if (Convert.ToByte(CheckoutSum & 0xFF) != Convert.ToByte(ReadData[DataLength+4]))
{
GloVar.FrameData_F = true;
MessageBox.Show("Data Frame Check Error.");
}
GloVar.CMD_H = ReadData[4];
GloVar.CMD_L = ReadData[5];
GloVar.RESULT_F = ReadData[6];
GloVar.ReadValidData = new byte[DataLength-3];
for (int i = 0; i < (DataLength-3); i++)
{
GloVar.ReadValidData[i] = ReadData[i+7];
}
if (ReadData[DataLength + 5].ToString("X") != "45" || ReadData[DataLength + 6].ToString("X") != "4E" || ReadData[DataLength + 7].ToString("X") != "44")
{
GloVar.FrameData_F = true;
MessageBox.Show("Data Frame End Error.");
}
}
public void WriteDataCRC(string WriteData)
{
int CheckoutSum = 0 ;
string WriteDataHex = WriteData.Replace(" ", "");
WriteDataHex += WriteDataHex.Length % 2 != 0 ? "0" : "";
int WriteValidDataLength = WriteDataHex.Length / 2;
GloVar.WriteValidData = new byte[WriteValidDataLength+8];
GloVar.WriteValidData[0] = 0x7E;
GloVar.WriteValidData[1] = 0x7E;
GloVar.WriteValidData[2] = Convert.ToByte((WriteValidDataLength >> 8) & 0xFF);
GloVar.WriteValidData[3] = Convert.ToByte(WriteValidDataLength & 0xFF);
for (int i=0; i<WriteValidDataLength; i++)
{
GloVar.WriteValidData[4+i] = Convert.ToByte(WriteDataHex.Substring(i * 2, 2), 16);
CheckoutSum += GloVar.WriteValidData[4+i];
}
GloVar.WriteValidData[4 + WriteValidDataLength] = Convert.ToByte(CheckoutSum & 0xFF);
GloVar.WriteValidData[5 + WriteValidDataLength] = 0x45;
GloVar.WriteValidData[6 + WriteValidDataLength] = 0x4E;
GloVar.WriteValidData[7 + WriteValidDataLength] = 0x44;
}
}
}
グローバル変数クラス、GlobalVariable.cs コードは次のとおりです。
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Wpf_USB.ViewModels;
namespace Wpf_USB.Models
{
class GlobalVariable : NotificationObject
{
private string globalSystemVersion = "1.0.0";
public string GlobalSystemVersion
{
get {
return globalSystemVersion; }
set
{
globalSystemVersion = value;
this.RaisePropertyChanged("GlobalSystemVersion");
}
}
private string globalUpdateTime = "2023.7.27";
public string GlobalUpdateTime
{
get {
return globalUpdateTime; }
set
{
globalUpdateTime = value;
this.RaisePropertyChanged("GlobalUpdateTime");
}
}
private static int cMD_H;
public int CMD_H
{
get {
return cMD_H; }
set
{
cMD_H = value;
this.RaisePropertyChanged("CMD_H");
}
}
private static int cMD_L;
public int CMD_L
{
get {
return cMD_L; }
set
{
cMD_L = value;
this.RaisePropertyChanged("CMD_L");
}
}
private static int rESULT_F;
public int RESULT_F
{
get {
return rESULT_F; }
set
{
rESULT_F = value;
this.RaisePropertyChanged("RESULT_F");
}
}
public static byte[] readValidData;
public byte[] ReadValidData
{
get {
return readValidData; }
set
{
readValidData = value;
this.RaisePropertyChanged("ReadValidData");
}
}
public static byte[] writeValidData;
public byte[] WriteValidData
{
get {
return writeValidData; }
set
{
writeValidData = value;
this.RaisePropertyChanged("WriteValidData");
}
}
public static bool frameData_F = false;
public bool FrameData_F
{
get {
return frameData_F; }
set
{
frameData_F = value;
this.RaisePropertyChanged("FrameData_F");
}
}
}
}
该Demo实现过程中,参考了一些大佬(CSDN和Github)的代码,在此表示感谢
この記事が皆様のお役に立てば幸いです。上記に何か間違っている点がございましたら、ご指摘ください。
共有が高さを決定し、学習が格差を広げる