大家知道WPF中多线程访问UI控件时会提示UI线程的数据不能直接被其他线程访问或者修改,该怎样来做呢?
分下面两种情况
1.WinForm程序
1)第一种方法,使用委托:
private delegate void SetTextCallback(string text); private void SetText(string text) { // InvokeRequired需要比较调用线程ID和创建线程ID // 如果它们不相同则返回true if (this.txt_Name.InvokeRequired) { SetTextCallback d = new SetTextCallback(SetText); this.Invoke(d, new object[] { text }); } else { this.txt_Name.Text = text; } } 2)第二种方法,使用匿名委托 private void SetText(Object obj) { if (this.InvokeRequired) { this.Invoke(new MethodInvoker(delegate { this.txt_Name.Text = obj; })); } else { this.txt_Name.Text = obj; } } 这里说一下BeginInvoke和Invoke和区别:BeginInvoke会立即返回,Invoke会等执行完后再返回。
2.WPF程序
1)可以使用Dispatcher线程模型来修改
如果是窗体本身可使用类似如下的代码:
this.lblState.Dispatcher.Invoke(new Action(delegate { this.lblState.Content = "状态:" + this._statusText; }));
那么假如是在一个公共类中弹出一个窗口、播放声音等呢?这里我们可以使用:System.Windows.Application.Current.Dispatcher,如下所示
System.Windows.Application.Current.Dispatcher.Invoke(new Action(() => { if (path.EndsWith(".mp3") || path.EndsWith(".wma") || path.EndsWith(".wav")) { _player.Open(new Uri(path)); _player.Play(); } }));
EmguCV中的Capture类可以完成视频文件的读取,并捕捉每一帧,可以利用Capture类完成实现WinForm中视频检测跟踪环境的搭建。本文只实现最简陋的WinForm + EmguCV上的avi文件读取和播放框架,复杂的检测和跟踪算法在之后添加进去。
这里使用WinForm实现视频的播放,主要是PictureBox类,它是支持基于事件的异步模式的典型组件,不使用EmguCV自带的UI控件等。
图1.效果图
直接在UI线程中完成视频的播放的话整个程序只有一个线程,由于程序只能同步执行,播放视频的时候UI将停止响应用户的输入,造成界面的假死。所以视频的播放需要实现异步模式。主要有三种方法:第一是使用异步委托;第二种是使用BackgroundWorker组件;最后一种就是使用多线程(不使用CheckForIllegalCrossThreadCalls =false的危险做法)。
Windows窗体控件,唯一可以从创建它的线程之外的线程中调用的是Invoke()、BegionInvoke()、EndInvoke()方法和InvokeRequired属性。其中BegionInvoke()、EndInvoke()方法是Invoke()方法的异步版本。这些方法会切换到创建控件的线程上,以调用赋予一个委托参数的方法,该委托参数可以传递给这些方法。
(一) 使用多线程
首先定义监控的类及其对应的事件参数类和异常类:
判断是否继续执行的布尔型成员会被调用线程改变,因此声名为volatile,不进行优化。
/// <summary> /// 红外检测子。 /// </summary> public class ThermalSurveillant { #region Private Fields /// <summary> /// 是否停止线程,此变量供多个线程访问。 /// </summary> private volatile bool shouldStop = false; #endregion #region Public Properties #endregion #region Public Events /// <summary> /// 帧刷新事件。 /// </summary> public EventHandler<FrameRefreshEventArgs> FrameRefresh; /// <summary> /// 播放完成。 /// </summary> public EventHandler<CompletedEventArgs> Completed; #endregion #region Protected Methods /// <summary> /// 处理帧刷新事件。 /// </summary> /// <param name="e"></param> protected virtual void OnFrameRefresh(FrameRefreshEventArgs e) { if (this.FrameRefresh != null) { this.FrameRefresh(this, e); } } /// <summary> /// 处理视频读完事件。 /// </summary> /// <param name="e"></param> protected virtual void OnCompleted(CompletedEventArgs e) { if (this.Completed != null) { this.Completed(this, e); } } #endregion #region Public Methods /// <summary> /// 视频监控。 /// </summary> /// <param name="capture">捕捉。</param> public void DoSurveillance(Object oCapture) { Capture capture = oCapture as Capture; int id = 1; if (capture == null) { throw new InvalidCaptureObjectException("传递的Capture类型无效。"); } while (!shouldStop) { Image<Bgr, byte> frame = capture.QueryFrame(); if (frame != null) { FrameRefreshEventArgs e = new FrameRefreshEventArgs(frame.ToBitmap(), id++); // 触发刷新事件 this.OnFrameRefresh(e); } else { break; } } // 触发完成事件 this.OnCompleted(new CompletedEventArgs(id)); } /// <summary> /// 请求停止线程。 /// </summary> public void Cancel() { this.shouldStop = true; } #endregion }
UI线程中启动播放线程:
声明:
/// <summary> /// 监控线程。 /// </summary> private Thread threadSurveillance = null; /// <summary> /// 捕获视频帧。 /// </summary> private Capture captureSurveillance; /// <summary> /// 监控子。 /// </summary> private ThermalSurveillant surveillant = new ThermalSurveillant();
读入视频文件:
captureSurveillance = new Capture(this.videoFilePath); captureSurveillance.SetCaptureProperty(CAP_PROP.CV_CAP_PROP_FRAME_WIDTH, this.width); captureSurveillance.SetCaptureProperty(CAP_PROP.CV_CAP_PROP_FRAME_HEIGHT, this.height); Image<Bgr, byte> frame = captureSurveillance.QueryFrame(); this.pictureBox.Image = frame.ToBitmap();
播放视频文件:
UI线程中响应监控类的事件:
定义异步调用的委托:
添加事件委托:
this.surveillant.FrameRefresh += OnRefreshFrame; this.surveillant.Completed += OnCompleted;
以下方法中都是由监控线程中的事件委托方法,应该使用BeginInvoke方法,这样可以优雅的结束线程,如果使用Invoke方法,则调用方式为同步调用,此时如果使用Thread.Join()方法终止线程将引发死锁(正常播放没有问题),Thread.Join()方法的使用使调用线程阻塞等待当前线程完成,在这里即UI线程阻塞等待监控线程完成,而监控线程中又触发UI线程中pictureBox的刷新,使用Invoke方法就造成了监控线程等待UI线程刷新结果,而UI线程已经阻塞,形成了死锁。死锁时只能用Thread.Abort()方法才能结束线程。或者直接强制结束应用程序。
使用BeginInvoke方法时为异步调用,监控线程不等待刷新结果直接继续执行,可以正常结束。结束后UI才进行刷新,不会造成死锁。
图2.线程关系
/// <summary> /// 刷新UI线程的pixtureBox的方法。 /// </summary> /// <param name="frame">要刷新的帧。</param> private void RefreshFrame(Bitmap frame) { this.pictureBox.Image = frame; // 这里一定不能刷新!2012年8月2日1:50:16 //this.pictureBox.Refresh(); } /// <summary> /// 响应pictureBox刷新。 /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private void OnRefreshFrame(object sender, FrameRefreshEventArgs e) { // 判断是否需要跨线程调用 if (this.pictureBox.InvokeRequired == true) { FrameRefreshDelegate fresh = this.RefreshFrame; this.BeginInvoke(fresh, e.Frame); } else { this.RefreshFrame(e.Frame); } } /// <summary> /// 响应Label刷新信息。 /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private void OnCompleted(object sender, CompletedEventArgs e) { // 判断是否需要跨线程调用 CompletedDelegate fresh = this.RefreshStatus; string message = "视频结束,共 " + e.FrameCount + " 帧。"; this.BeginInvoke(fresh, message); } 关闭时需要中止播放线程之后再退出: /// <summary> /// 关闭窗体时发生。 /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private void OnFormClosed(object sender, FormClosedEventArgs e) { // 检测子算法请求终止 surveillant.Cancel(); // 阻塞调用线程直到检测子线程终止 if (threadSurveillance != null) { if (threadSurveillance.IsAlive == true) { threadSurveillance.Join(); } } }
(二) 使用异步委托
创建线程的一个更简单的方法是定义一个委托,并异步调用它。委托是方法的类型安全的引用。Delegate类还支持异步地调用方法。在后台,Delegate类会创建一个执行任务的线程。
// asynchronous by using a delegate PlayVideoDelegate play = this.PlayVideoFile; IAsyncResult status = play.BeginInvoke(null, null); /// <summary> /// 播放视频文件。 /// </summary> private void PlayVideoFile() { while (true) { Image<Bgr, byte> frame = capture.QueryFrame(); if (frame != null) { Image<Gray, byte> grayFrame = frame.Convert<Gray, byte>(); grayFrame.Resize(this.width, this.height, INTER.CV_INTER_CUBIC); RefreshPictureBoxDelegate fresh = this.RefreshPictureBox; try { this.BeginInvoke(fresh, grayFrame.ToBitmap()); } catch (ObjectDisposedException ex) { Thread.CurrentThread.Abort(); } } else { break; } } } /// <summary> /// 刷新UI线程的pixtureBox的方法。 /// </summary> /// <param name="frame">要刷新的帧。</param> private void RefreshPictureBox(Bitmap frame) { this.pictureBox.Image = frame; }
(三) 使用BackgroundWorker组件
BackgroundWorker类是异步事件的一种实现方案,异步组件可以选择性的支持取消操作,并提供进度信息。RunWorkerAsync()方法启动异步调用。CancelAsync()方法取消。
图3.BackgroundWorker组件
/// <summary>
/// 播放视频文件。 /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private void detectItemPlay_Click(object sender, EventArgs e) { if (this.videoFilePath != null) { // run async this.backgroundWorker.RunWorkerAsync(capture); } } /// <summary> /// 异步调用。 /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private void OnDoWork(object sender, DoWorkEventArgs e) { Emgu.CV.Capture capture = e.Argument as Emgu.CV.Capture; while (!e.Cancel) { Image<Bgr, byte> frame = capture.QueryFrame(); if (frame != null) { Image<Gray, byte> grayFrame = frame.Convert<Gray, byte>(); grayFrame.Resize(this.width, this.height, INTER.CV_INTER_CUBIC); if (this.backgroundWorker.CancellationPending == true) { e.Cancel = true; break; } else { if (this.pictureBox.InvokeRequired == true) { RefreshPictureBoxDelegate fresh = this.RefreshPictureBox; this.BeginInvoke(fresh, grayFrame.ToBitmap()); } else { this.RefreshPictureBox(grayFrame.ToBitmap()); } } } else { break; } } } /// <summary> /// 关闭窗体时发生。 /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private void MainForm_FormClosed(object sender, FormClosedEventArgs e) { if (this.backgroundWorker.IsBusy) { this.backgroundWorker.CancelAsync(); } }
转自http://blog.csdn.net/azkabannull/article/details/7827673