こんにちは。
Windows10でVisual Studio 2017 Proを使って開発しています。
C# WPF(MVVM)で別スレッドからビットマップイメージを表示する際に問題が
発生しているため、お力添えをお願いします。
前提・実現したいこと
C# WPFで、MVVMの構成でTaskを使った別スレッドから受け取ったビットマップイメージを"1枚ずつ"表示したいです。
C#のWPF、MVVMの構成で、外部(PCとUSB接続している機械)から受信したビットマップイメージを表示しようとしています。
受信するビットマップイメージは複数枚(枚数は毎回違う)で、受信タイミングも不定期です。
データが複数枚且つ不定期受信であること、USB通信や受信データのチェック処理を行うこと、受信に成功したデータを
順次表示するため、Task.Runを使用してViewModelからModelのメソッドを呼び出すことで、別スレッドで処理しています。
1度の操作で複数枚のイメージデータを表示するため、受信したデータはデリゲートと使ってModelからViewModelへ渡し
ViewModelのでウィンドウのコントロールにバインドすることで表示しようとしています。
処理のイメージは以下通りです。
1.ボタンクリック ↓ 2.ViewModelからModelのビットマップイメージ受信メソッドを呼び出す (ここでTask.Runを使用して別スレッド化) ↓ 3.ModelからUSB通信処理を呼び出す ↓ 4.USB通信でビットマップイメージデータを受信 ↓ 5.Modelへデリゲートで受信したデータを渡す ↓ 6.USB通信処理から受け取ったデータをファイル出力 ↓ 7.ModelからViewModelへデリゲートで受信したデータを渡す ↓ 8.Modelから受け取ったビットマップイメージデータをコントロールにバインドして表示 (ビットマップイメージのコントロールは1つのため、以降データを上書いていく) ↓ 4~8をデータを受信し終えるまで繰り返す ↓ 9.データ受信を終えたら処理をViewModelへ戻す
発生している問題
1枚分のビットマップイメージを受信して表示すること、一定間隔(3秒以上)で受信して表示することには
成功しているのですが、受信間隔が短い(3秒未満)場合に2枚目以降のビットマップイメージが表示されません。
表示されない状態というのは、1枚目のビットマップイメージが消え、ビットマップイメージ表示領域が
白背景の状態です。
エラーメッセージの表示やアプリケーションが落ちることはなく、2枚目以降が表示さず動き続けています。
上記、「前提・実現したいこと」に記載の処理イメージの6.に記載しているファイル出力は正常に行われており、
出力したファイルのデータは、別のビットマップイメージを表示するツールで確認すると問題なく表示できます。
おそらく、ViewModelでビットマップイメージをコントロールにバインドしている処理あたりが原因かとおもうのですが、
なぜ2枚目以降が表示されないのか原因が分かりません。
該当のソースコード
以下、実際のソースコードから問題に関連する箇所のみを掲載しています。
Window.xaml(View)
C#
1<!-- ImageタグのSourceにビットマップイメージデータをバインドする --> 2<Image Source="{Binding BitmapImageData}" Stretch="Uniform"/> 3 4<!-- データ受信実行ボタンは省略 --> 5
Window.xaml.xs
C#
1private ViewModel m_viewModel; // ViewModelクラス 2public Window() 3{ 4 InitializeComponent(); 5 6 m_viewModel = new ViewModel(); 7 DataContext = m_viewModel; 8}
ViewModelBase(ViewModelの基底クラス)
C#
1// ViewModelのプロパティにセットされたデータをViewへ反映する 2public abstract class ViewModelBase : INotifyPropertyChanged, INotifyDataErrorInfo 3{ 4 ~ViewModelBase() 5 { 6 ClearResource(); 7 } 8 9 private bool _isCrlResource = false; 10 11 protected virtual void ClearResource() 12 { 13 if (_isCrlResource) return; 14 15 if (PropertyChanged != null) 16 { 17 foreach (var handler in PropertyChanged.GetInvocationList()) 18 { 19 PropertyChanged -= (PropertyChangedEventHandler)handler; 20 } 21 } 22 23 PropertyChanged = null; 24 _isCrlResource = true; 25 } 26 27 /// <summary> 28 /// INotifyPropertyChanged.PropertyChanged の実装。 29 /// INotifyPropertyChanged.PropertyChanged implement. 30 /// </summary> 31 public event PropertyChangedEventHandler PropertyChanged; 32 33 /// <summary> 34 /// INotifyPropertyChanged.PropertyChangedイベントを発生させる。 35 /// INotifyPropertyChanged.PropertyChanged event is generated. 36 /// </summary> 37 protected virtual bool SetProperty<T>(ref T storage, T value, [CallerMemberName] String propertyName = null) 38 { 39 if (object.Equals(storage, value)) return false; 40 41 storage = value; 42 43 if (PropertyChanged != null) 44 { 45 PropertyChanged.Invoke(this, new PropertyChangedEventArgs(propertyName)); 46 } 47 else 48 { 49 PropertyChanged = null; 50 } 51 52 return true; 53 } 54}
WindowViewModel.cs(ViewModel)
C#
1// ViewModelBase->INotifyPropertyChanged等を実装した基底クラス 2public class WindowViewModel : ViewModelBase 3{ 4 private WindowModel m_model; // Modelクラス 5 6 private BitmapImage _bitmapImageData; 7 // 受信したビットマップイメージをバインドするプロパティ 8 public BitmapImage BitmapImageData 9 { 10 get { return _bitmapImageData; } 11 set { SetProperty(ref _bitmapImageData, value); } 12 } 13 14 public WindowViewModel() 15 { 16 m_model = new WindowModel(); 17 m_model.SetDelegateFunc(DisplayBitmapImage); 18 BitmapImageData = null; 19 } 20 21 private async void ReadBitmapImage() 22 { 23 // ボタングレーアウト処理 24 25 await Task.Run(() => m_model.ReadBitmapImageData()); 26 27 // ボタングレーアウト解除処理 28 } 29 30 // Modelからビットマップイメージを受け取るデリゲート 31 private void DisplayBitmapImage(byte[] bitmapData) 32 { 33 BitmapImage bitmapImg = null; 34 try 35 { 36 using (var memStream = new System.IO.MemoryStream(bitmapData)) 37 { 38 bitmapImg = new BitmapImage(); 39 bitmapImg.BeginInit(); 40 bitmapImg.CacheOption = BitmapCacheOption.OnLoad; 41 bitmapImg.StreamSource = memStream; 42 bitmapImg.EndInit(); 43 bitmapImg.Freeze(); 44 } 45 } 46 catch (Exception) 47 { 48 // エラー処理 49 bitmapImg = null; 50 } 51 52 BitmapImageData = bitmapImg; // ビットマップコントロールにデータをセット 53 } 54}
WindowModel.cs(Model)
C#
1public class WindowModel() 2{ 3 private Action<byte[]> m_displayBitmap; // ViewModelのビットマップイメージ表示メソッド 4 private byte[] m_bitmapData; // ビットマップイメージデータ 5 6 public WindowModel() 7 { 8 m_displayBitmap = null; 9 m_bitmapData = null; 10 } 11 12 public void ReadBitmapImageData() 13 { 14 USBComm.SetDelegateFunc(DisplayBitmapImage); // イメージデータを取得するためのデリゲート設定 15 USBComm.ReadImageData(); // イメージデータ取得 16 } 17 18 // USB通信処理クラスからビットマップイメージを受け取るデリゲート 19 private void DisplayBitmapImage(uint dataSize, bool dataErrFlag) 20 { 21 // 受信したデータに異常がない場合のみDLLから取得する 22 if (dataErrFlag == false) 23 { 24 byte[] buff = new byte[dataSize]; 25 USBComm.GetReceiveData(ref buff); 26 27 m_bitmapData = bitmapData; // 意味ないと思うが念のためメンバにセット 28 m_displayBitmap(m_bitmapData); // Viewのビットマップイメージ表示メソッド呼び出し 29 m_bitmapData = null; 30 } 31 32 USBComm.ClearReceiveData(); // DLL内のデータをクリア 33 } 34}
USBComm.cs(USB通信処理DLLインターフェイス)
C#
1public static class USBComm 2{ 3 /// <summary> 4 /// DLL内で取得したイメージデータのサイズとエラー情報を取得するデリゲート 5 /// </summary> 6 /// <param name="dataSize">イメージデータサイズ</param> 7 /// <param name="errFlag">データエラーフラグ : 0 = Not error / !0 = Error</param> 8 public delegate void DelegateDisplayBitmapImage(uint dataSize, byte errFlag); 9 10 static DelegateDisplayBitmapImage m_delegateFunc; 11 12 // イメージデータ取得用デリゲートメソッドのセット 13 [DllImport("USBComm.dll", EntryPoint = "SetDelegateFunc", CallingConvention = CallingConvention.StdCall)] 14 private static extern void DllSetDelegateFunc(DelegateDisplayBitmapImage func); 15 // イメージデータ受信 16 [DllImport("USBComm.dll", EntryPoint = "ReadImageData", CallingConvention = CallingConvention.StdCall)] 17 private static extern void DllReadImageData(); 18 // 受信データ取得 19 [DllImport("USBComm.dll", EntryPoint = "GetReceiveData", CallingConvention = CallingConvention.StdCall)] 20 private static extern void DllGetReceiveData(IntPtr iPtr); 21 // 受信データクリア 22 [DllImport("USBComm.dll", EntryPoint = "ClearReceiveData", CallingConvention = CallingConvention.StdCall)] 23 private static extern void DllClearReceiveData(); 24 25 public static void SetDelegateFunc(DelegateDisplayBitmapImage func) 26 { 27 m_delegateFunc = func; 28 DllSetDelegateFunc(m_delegateFunc ); 29 } 30 31 public static void ReadImageData() => DllReadImageData(); 32 33 public static void GetReceiveData(ref byte[] buff) 34 { 35 // 取得するデータサイズ分IntPtrのメモリ領域を確保 36 IntPtr ptr = Marshal.AllocHGlobal(buff.Length); 37 // DLLからデータを取得 38 DllGetReceiveData(ptr); 39 // DLLから取得したデータをコピー 40 Marshal.Copy(iPtr, buff, 0, buff.Length); 41 // IntPtrのメモリ解放 42 Marshal.FreeHGlobal(ptr); 43 } 44 45 public static void ClearReceiveData() => DllClearReceiveData; 46}
USBCommクラスで参照しているDLL(USBComm.dll)はC++で実装しています。
DLLのソースコードは、セキュリティの問題で掲載できません。
同じDLLを使用したC++ MFCのツールが存在し、そちらで同じように複数枚のイメージデータを
非同期処理で受信→表示することができています。
確認したこと
Taskを使用した別スレッドでUIスレッド外からのアクセスのため、表示ができない
(1枚目や一定間隔はできているが)のかと思いネットで情報を探してみたのですが、
MVVMでバインディングを介してウィンドウを更新する場合は問題ないという内容が
いくつか見つかりました。
ビットマップイメージを表示するための"Imageタグ"のプロパティにデータがセット
されていることはデバッグで確認しているのですが、先に表示されていたイメージが
消え白背景に更新されてしまいます。
そのため、現状何が原因か分からず、手詰まりの状態です。
補足情報(FW/ツールのバージョンなど)
開発環境
Windows 10 Pro
Microsoft Visual Studio Prodessional 2017 Version 15.9.15
.NET Framework Version 4.8.03752
ターゲットフレームワーク 4.5
言語バージョン C# 6