WPF NotifyIcon来自后台线程

时间:2015-07-22 16:51:42

标签: wpf

我知道通常不应该从UI线程以外的线程触摸UI元素,但我是WPF的新手,我想知道我当前的工作实现是否可以改进。

我的应用程序仅包含通知托盘图标,我想从后台线程更新该图标。

这是我的Program.cs入口点:

static class Program
{
    [STAThread]
    static void Main()
    {
        Application.EnableVisualStyles();
        Application.SetCompatibleTextRenderingDefault(false);

        using (IconHandler notify = new IconHandler())
        {
            notify.Display();
            Application.Run();
        }

    }
}

这是我的IconHandler.cs通知图标处理程序类:

class IconHandler : IDisposable
{

    NotifyIcon ni;

    public IconHandler()
    {
        ni = new NotifyIcon();
    }

    public void Display()
    {
        ni.MouseClick += new MouseEventHandler(ni_MouseClick);
        ni.Icon = Resources.icon1;
        ni.Visible = true;

        new Thread(new ThreadStart(UpdateIcon)).Start();
    }

    public void UpdateIcon()
    {
        while (true)
        {
            // reference ni directly, it updates fine
        }
    }

    public void Dispose()
    {
        ni.Dispose();
    }

    void ni_MouseClick(object sender, MouseEventArgs e)
    {
        // something useful
    }
}

这有什么明显不正确的吗?这对我来说似乎有点可疑 - 这只是我的第一次尝试。它似乎适合我想做的事情,有没有人有任何更好的实施建议?我会在这个设置中遇到生命周期问题吗?

3 个答案:

答案 0 :(得分:3)

这有什么明显不正确的吗?这对我来说似乎有点可疑 - 这只是我的第一次尝试。它似乎适合我想做的事情,有没有人有任何更好的实施建议?我会在这个设置中遇到生命周期问题吗?

首先,NotifyIcon不是WPF控件,而是来自Windows窗体命名空间。因此,它具有正常的C#属性(例如IconVisible),这意味着您可以在非UI线程中更改图标属性,而不会引发异常。如果您使用了WPF控件,那么它们具有依赖项属性,并且在UI线程之外直接操作依赖项属性将导致异常被引发。

我会在这个设置中遇到生命周期问题吗?

您目前 NOT 创建了WPF窗口或WPF控件。如果您的应用程序开发使得您开始使用WPF并且UpdateIcon方法扩展为比您当前执行的更多并访问这些WPF对象,那么您将需要一种策略来处理来自非UI线程的更新。

您可以使用一些辅助方法隐藏一些此跨线程访问。


示例1 如果您的策略从后台线程以编程方式引用WPF控件,那么您可以使用诸如此类的辅助方法。

它首先检查调用是否在UI线程上,如果是,则它直接更新控件,否则它将安排稍后从UI线程调用方法(本身)时间点。

我在这里使用了BeginInvoke,以便后台线程可以在UI线程实际调用该方法之前继续。如果要阻止后台线程,请改用Invoke

public void UpdateLabel(Label control, string text)
{ 
  if (Application.Current.Dispatcher.CheckAccess()) 
    control.Content = text;
  else
    Application.Current.Dispatcher.BeginInvoke(new System.Action(() => UpdateLabel(control, text)), DispatcherPriority.Normal);
}


示例2 如果您的策略使用后台线程上引发的Events以编程方式更新WPF控件,那么您可以隐藏一些跨线程调用作为引发事件的一部分,使WPF更新例程非常干净且易于阅读。

可以编码此事件的任何事件处理程序,因为它将从UI线程进行调用,因此不会出现线程问题。

public void OnRaiseEvent(EventHandler handler, EventArgs args)
{
  if (handler != null)
    {
    if (Application.Current.Dispatcher.CheckAccess())
      handler(sender, new PropertyChangedEventArgs(propName));
    else
      Application.Current.Dispatcher.BeginInvoke(new System.Action(() => handler(sender, args)), DispatcherPriority.Normal);
     }
}


示例3 如果您的未来策略充分利用WPF与Binding的优势(而不是以编程方式更新WPF控件),那么您可以将交叉线程代码嵌入到数据绑定对象中。

例如,如果您的XAML数据绑定到MyProperty类实例的MyDataClass属性,并且该类实现了INotifyPropertyChanged接口,则可以将交叉线程代码放入数据中class使得可以从任何线程更新数据。以下是该类的示例: -

public class MyDataClass : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;

    private string _myProperty;

    public string MyProperty { get { return _myProperty;} set { PropertyChanged.SetValueAndNotify(this, ref _myProperty, value); } }
}

此课程在SetValueAndNotify事件中使用PropertyChanged扩展名方法。在这里我们隐藏了交叉线程代码以简化代码的其他部分。这是此扩展方法的定义。

public static class PropertyChangedExtension
{
    public static void SetValueAndNotify<T>(this PropertyChangedEventHandler handler, object sender, ref T destination, T source, [CallerMemberName] string propName = "notset")
    {
        // Is the new value different from the previous value? If there is no difference then there is nothing more to do
        if (Equals(destination, source))
            return;

        // If we got to this point then the new value is different from the old value, so lets make the assignemnt and raise the property changed event
        destination = source;

        if (handler != null)
        {
            if (Application.Current.Dispatcher.CheckAccess())
                handler(sender, new PropertyChangedEventArgs(propName));
            else
                Application.Current.Dispatcher.BeginInvoke(new System.Action(() => handler(sender, new PropertyChangedEventArgs(propName))), DispatcherPriority.Normal);
        }
    }
}

上面的示例使用C#5中的[CallerMemberName]属性来删除为INotifyPropertyChanged参数提供属性名称时的任何输入错误。如果您没有使用最新版本,则需要按如下方式修改getter和setter: -

public string MyProperty { get { return _myProperty;} set { PropertyChanged.SetValueAndNotify(this, ref _myProperty, value, "MyProperty"); } }

答案 1 :(得分:0)

代码中的while(true)循环将导致大量CPU /资源使用。也许添加例如Thread.Sleep(1000)进入循环以允许更新之间的中断。

后台线程的最佳用法是在后台线程上执行长时间运行的工作(例如与服务器/ DB的通信),一旦线程完成,让UI线程更新UI。

使用BackgroundWorker:

var worker = new BackgroundWorker();

worker.DoWork += (sender, args) =>
{
    // long running work
};

worker.RunWorkerCompleted += (sender, args) =>
{
    // Update UI
};

worker.RunWorkerAsync();

async / await pattern:

public async void DoWork()
{
    // Do long running task
    var data = await Task.Run(() => new object());

    // Update UI here
}

TaskFactory:

Task.Factory.StartNew(() => new Object()).ContinueWith(task => MessageBox.Show(task.Result.ToString()), TaskScheduler.FromCurrentSynchronizationContext());

如果UI需要在常量循环上更新,可以使用计时器定期重启该过程。这样可以避免CPU受到冲击。

答案 2 :(得分:0)

您必须始终只从UI线程更新UI,但是,您可以使用调度程序从后台线程安排UI线程的一些工作

public void Display()
{
    ni.MouseClick += new MouseEventHandler(ni_MouseClick);
    ni.Icon = Resources.icon1;
    ni.Visible = true;

    new Thread(new ThreadStart(UpdateIcon)).Start();
}

public void UpdateIcon()
{
    while (true)
    {
        //do some long running work
        Application.Current.Dispatcher.Invoke(()=>{
           //update ui
        });
    }
}

但是如果你没有长时间的工作并且你只想定期做某事,你应该在后台线程中使用DispatcherTimer而不是循环。