c# - واجهات - واجهة المستخدم الرسومية ماتلاب pdf




كيف يمكنني تحديث واجهة المستخدم الرسومية من مؤشر ترابط آخر؟ (20)

ما هي أبسط طريقة لتحديث Label من مؤشر ترابط آخر؟

لدي Form على thread1 ، ومن ذلك أنا بدأت موضوع آخر ( thread2 ). بينما يقوم thread2 بمعالجة بعض الملفات التي أرغب في تحديثها في Form باستخدام الحالة الحالية thread2 .

كيف أقوم بذلك؟


التعامل مع العمل الطويل

بما أن .NET 4.5 و C # 5.0 يجب عليك استخدام نمط Asynchronous Pattern (TAP) القائم على المهمة مع async - في await الكلمات الرئيسية في جميع المناطق (بما في ذلك واجهة المستخدم الرسومية):

TAP هو نمط التصميم غير المتزامن الموصى به للتطوير الجديد

بدلاً من طراز البرمجة غير المتزامنة (APM) و "نمط متزامن" المستند إلى الأحداث (EAP) (يتضمن الأخير فئة BackgroundWorker ).

ثم ، الحل الموصى به لتطوير جديد هو:

  1. التنفيذ غير المتزامن لمعالِج أحداث (نعم ، هذا كل شيء):

    private async void Button_Clicked(object sender, EventArgs e)
    {
        var progress = new Progress<string>(s => label.Text = s);
        await Task.Factory.StartNew(() => SecondThreadConcern.LongWork(progress),
                                    TaskCreationOptions.LongRunning);
        label.Text = "completed";
    }
    
  2. تنفيذ سلسلة المحادثات الثانية التي تقوم بإعلام مؤشر ترابط واجهة المستخدم:

    class SecondThreadConcern
    {
        public static void LongWork(IProgress<string> progress)
        {
            // Perform a long running work...
            for (var i = 0; i < 10; i++)
            {
                Task.Delay(500).Wait();
                progress.Report(i.ToString());
            }
        }
    }
    

لاحظ ما يلي:

  1. شفرة قصيرة ونظيفة مكتوبة بطريقة متتالية دون عمليات رد الاتصال والخيوط الواضحة.
  2. Task بدلا من Thread .
  3. الكلمة الأساسية async ، التي تسمح باستخدام await بدوره منع معالج الحدث من الوصول إلى حالة الإكمال حتى تنتهي المهمة وفي الوقت نفسه لا يمنع مؤشر ترابط واجهة المستخدم.
  4. درجة التقدم (انظر واجهة IProgress ) التي تدعم مبدأ تصميم فصل المخاوف (SoC) ولا تتطلب مرسل صريح واستدعاء. ويستخدم SynchronizationContext الحالي من مكان إنشائه (هنا مؤشر ترابط واجهة المستخدم).
  5. TaskCreationOptions.LongRunning تلميحات إلى عدم قائمة المهمة في ThreadPool .

للحصول على أمثلة مطولة أكثر ، أنظر: The Future of C #: الأشياء الجيدة تأتي إلى أولئك الذين "ينتظرون" بقلم جوزيف البهاري .

راجع أيضًا مفهوم UI Threading Model .

التعامل مع الاستثناءات

يعتبر المقتطف أدناه مثالاً لكيفية التعامل مع الاستثناءات والخاصية Enabled لزر التبديل لمنع تكرار النقرات أثناء تنفيذ الخلفية.

private async void Button_Click(object sender, EventArgs e)
{
    button.Enabled = false;

    try
    {
        var progress = new Progress<string>(s => button.Text = s);
        await Task.Run(() => SecondThreadConcern.FailingWork(progress));
        button.Text = "Completed";
    }
    catch(Exception exception)
    {
        button.Text = "Failed: " + exception.Message;
    }

    button.Enabled = true;
}

class SecondThreadConcern
{
    public static void FailingWork(IProgress<string> progress)
    {
        progress.Report("I will fail in...");
        Task.Delay(500).Wait();

        for (var i = 0; i < 3; i++)
        {
            progress.Report((3 - i).ToString());
            Task.Delay(500).Wait();
        }

        throw new Exception("Oops...");
    }
}

Salvete! بعد البحث عن هذا السؤال ، وجدت إجابات FrankG و Oregon Ghost أسهل طريقة بالنسبة لي. الآن ، أنا رمز في Visual Basic وتشغيل هذا المقتطف من خلال محول؛ لذلك أنا لست متأكدة تماما كيف تبين ذلك.

لدي نموذج حوار يسمى form_Diagnostics, والذي يحتوي على مربع updateDiagWindow, يسمى updateDiagWindow, والذي أستخدمه كنوع من عرض التسجيل. كنت بحاجة إلى أن أكون قادراً على تحديث النص الخاص به من كافة مؤشرات الترابط. تسمح الخطوط الإضافية للنافذة بالتمرير تلقائيًا إلى أحدث الأسطر.

وهكذا ، يمكنني الآن تحديث الشاشة بخط واحد ، من أي مكان في البرنامج بالكامل بالطريقة التي تعتقد أنها ستعمل دون أي ترابط:

  form_Diagnostics.updateDiagWindow(whatmessage);

الكود الرئيسي (ضع هذا الكود داخل نموذج طبقتك):

#region "---------Update Diag Window Text------------------------------------"
// This sub allows the diag window to be updated by all threads
public void updateDiagWindow(string whatmessage)
{
    var _with1 = diagwindow;
    if (_with1.InvokeRequired) {
        _with1.Invoke(new UpdateDiagDelegate(UpdateDiag), whatmessage);
    } else {
        UpdateDiag(whatmessage);
    }
}
// This next line makes the private UpdateDiagWindow available to all threads
private delegate void UpdateDiagDelegate(string whatmessage);
private void UpdateDiag(string whatmessage)
{
    var _with2 = diagwindow;
    _with2.appendtext(whatmessage);
    _with2.SelectionStart = _with2.Text.Length;
    _with2.ScrollToCaret();
}
#endregion

الحل البسيط هو استخدام Control.Invoke .

void DoSomething()
{
    if (InvokeRequired) {
        Invoke(new MethodInvoker(updateGUI));
    } else {
        // Do Something
        updateGUI();
    }
}

void updateGUI() {
    // update gui here
}

الغالبية العظمى من الإجابات تستخدم Control.Invoke وهي حالة سباق ينتظر حدوثها . على سبيل المثال ، ضع في الاعتبار الإجابة المقبولة:

string newText = "abc"; // running on worker thread
this.Invoke((MethodInvoker)delegate { 
    someLabel.Text = newText; // runs on UI thread
});

إذا قام المستخدم بإغلاق النموذج قبل ذلك this.Invoke يتم استدعاء this.Invoke (تذكر ، this هو كائن Form ) ، من المحتمل أن يتم تشغيل ObjectDisposedException .

الحل هو استخدام SynchronizationContext ، وبالتحديد SynchronizationContext.Current كما يقترح hamilton.danielb (تعتمد إجابات أخرى على تطبيقات SynchronizationContext محددة وهي غير ضرورية تمامًا). سأقوم بتعديل التعليمات البرمجية الخاصة به قليلاً لاستخدام SynchronizationContext.Post بدلاً من SynchronizationContext.Send بالرغم من ذلك (كما لا يوجد عادةً حاجة مؤشر ترابط العامل الانتظار):

public partial class MyForm : Form
{
    private readonly SynchronizationContext _context;
    public MyForm()
    {
        _context = SynchronizationContext.Current
        ...
    }

    private MethodOnOtherThread()
    {
         ...
         _context.Post(status => someLabel.Text = newText,null);
    }
}

لاحظ أنه في .NET 4.0 والإصدارات الأحدث يجب أن تستخدم المهام لعمليات التزامن. راجع إجابة n-san's للنهج القائم على المهام المكافئة (باستخدام TaskScheduler.FromCurrentSynchronizationContext ).

أخيرًا ، على .NET 4.5 وما بعده ، يمكنك أيضًا استخدام Progress<T> (والذي يلتقط بشكل أساسي SynchronizationContext.Current عند إنشائه) كما هو موضح بواسطة Ryszard Dżegan للحالات التي تحتاج فيها العملية التي طال أمدها إلى تشغيل كود واجهة المستخدم أثناء العمل.


بالنسبة إلى .NET 2.0 ، إليك جزءًا رائعًا من التعليمات البرمجية التي كتبتها تمامًا كما تريد ، وتعمل مع أي خاصية على جهاز Control :

private delegate void SetControlPropertyThreadSafeDelegate(
    Control control, 
    string propertyName, 
    object propertyValue);

public static void SetControlPropertyThreadSafe(
    Control control, 
    string propertyName, 
    object propertyValue)
{
  if (control.InvokeRequired)
  {
    control.Invoke(new SetControlPropertyThreadSafeDelegate               
    (SetControlPropertyThreadSafe), 
    new object[] { control, propertyName, propertyValue });
  }
  else
  {
    control.GetType().InvokeMember(
        propertyName, 
        BindingFlags.SetProperty, 
        null, 
        control, 
        new object[] { propertyValue });
  }
}

يطلق عليه مثل هذا:

// thread-safe equivalent of
// myLabel.Text = status;
SetControlPropertyThreadSafe(myLabel, "Text", status);

إذا كنت تستخدم .NET 3.0 أو أعلى ، فيمكنك إعادة كتابة الطريقة السابقة كأسلوب ملحق لفئة Control ، مما يؤدي إلى تبسيط الاستدعاء إلى:

myLabel.SetPropertyThreadSafe("Text", status);

التحديث 05/10/2010:

بالنسبة إلى .NET 3.0 ، يجب استخدام هذا الرمز:

private delegate void SetPropertyThreadSafeDelegate<TResult>(
    Control @this, 
    Expression<Func<TResult>> property, 
    TResult value);

public static void SetPropertyThreadSafe<TResult>(
    this Control @this, 
    Expression<Func<TResult>> property, 
    TResult value)
{
  var propertyInfo = (property.Body as MemberExpression).Member 
      as PropertyInfo;

  if (propertyInfo == null ||
      [email protected]().IsSubclassOf(propertyInfo.ReflectedType) ||
      @this.GetType().GetProperty(
          propertyInfo.Name, 
          propertyInfo.PropertyType) == null)
  {
    throw new ArgumentException("The lambda expression 'property' must reference a valid property on this Control.");
  }

  if (@this.InvokeRequired)
  {
      @this.Invoke(new SetPropertyThreadSafeDelegate<TResult> 
      (SetPropertyThreadSafe), 
      new object[] { @this, property, value });
  }
  else
  {
      @this.GetType().InvokeMember(
          propertyInfo.Name, 
          BindingFlags.SetProperty, 
          null, 
          @this, 
          new object[] { value });
  }
}

الذي يستخدم تعبيرات LINQ و lambda للسماح ببناء أكثر نظافة وبساطة وأمانًا:

myLabel.SetPropertyThreadSafe(() => myLabel.Text, status); // status has to be a string or this will fail to compile

ليس فقط اسم الخاصية محدد الآن في وقت التحويل البرمجي ، نوع الخاصية كذلك ، لذا من المستحيل (على سبيل المثال) تعيين قيمة سلسلة لخاصية منطقية ، وبالتالي تسبب استثناء وقت التشغيل.

لسوء الحظ هذا لا يمنع أي شخص من القيام بأشياء غبية مثل تمرير خاصية وقيمة Control أخرى ، لذلك سوف يجمع ما يلي بسعادة:

myLabel.SetPropertyThreadSafe(() => aForm.ShowIcon, false);

وبالتالي أضفت عمليات التحقق من وقت التشغيل للتأكد من أن الخاصية التي تم تمريرها تنتمي بالفعل إلى Control الذي يتم استدعاء الأسلوب عليه. ليس مثاليا ، ولكن لا يزال أفضل بكثير من إصدار .NET 2.0.

إذا كان لدى أي شخص أي اقتراحات أخرى حول كيفية تحسين هذا الرمز لضمان سلامة وقت التحويل ، يرجى التعليق!


بالنسبة للعديد من الأغراض ، الأمر بسيط مثل:

public delegate void serviceGUIDelegate();
private void updateGUI()
{
  this.Invoke(new serviceGUIDelegate(serviceGUI));
}

"serviceGUI ()" هو أسلوب مستوى واجهة المستخدم الرسومية داخل النموذج (هذا) الذي يمكنه تغيير العديد من عناصر التحكم كما تريد. استدعاء "updateGUI ()" من سلسلة المحادثات الأخرى. يمكن إضافة المعلمات لتمرير القيم ، أو (ربما بشكل أسرع) استخدام متغيرات نطاق الطبقة مع التأمين عليها كما هو مطلوب إذا كان هناك أي احتمال لواجهة بين مؤشرات الترابط الوصول إليها والتي يمكن أن تسبب عدم الاستقرار. استخدم BeginInvoke بدلاً من Invoke إذا كان مؤشر ترابط واجهة المستخدم الرسومية غير وقت حرج (مع مراعاة تحذير بريان جدعون).


ستحتاج إلى استدعاء الأسلوب على مؤشر ترابط واجهة المستخدم الرسومية. يمكنك القيام بذلك عن طريق استدعاء Control.Invoke.

فمثلا:

delegate void UpdateLabelDelegate (string message);

void UpdateLabel (string message)
{
    if (InvokeRequired)
    {
         Invoke (new UpdateLabelDelegate (UpdateLabel), message);
         return;
    }

    MyLabelControl.Text = message;
}

عندما واجهت نفس المشكلة ، طلبت المساعدة من Google ، ولكن بدلاً من إعطائي حلًا بسيطًا ، أربكتني أكثر من خلال إعطاء أمثلة على MethodInvoker و blah blah blah. لذلك قررت حلها بمفردي. هنا هو الحل الخاص بي:

جعل مندوب مثل هذا:

Public delegate void LabelDelegate(string s);

void Updatelabel(string text)
{
   if (label.InvokeRequired)
   {
       LabelDelegate LDEL = new LabelDelegate(Updatelabel);
       label.Invoke(LDEL, text);
   }
   else
       label.Text = text
}

You can call this function in a new thread like this

Thread th = new Thread(() => Updatelabel("Hello World"));
th.start();

Don't be confused with Thread(() => .....) . I use an anonymous function or lambda expression when I work on a thread. To reduce the lines of code you can use the ThreadStart(..) method too which I am not supposed to explain here.


لا شيء من الأشياء Invoke في الإجابات السابقة ضروري.

تحتاج إلى إلقاء نظرة على WindowsFormsSynchronizationContext:

// In the main thread
WindowsFormsSynchronizationContext mUiContext = new WindowsFormsSynchronizationContext();

...

// In some non-UI Thread

// Causes an update in the GUI thread.
mUiContext.Post(UpdateGUI, userData);

...

void UpdateGUI(object userData)
{
    // Update your GUI controls here
}

هذا في صيغة C # 3.0 الخاصة بي من حل Ian Kemp:

public static void SetPropertyInGuiThread<C,V>(this C control, Expression<Func<C, V>> property, V value) where C : Control
{
    var memberExpression = property.Body as MemberExpression;
    if (memberExpression == null)
        throw new ArgumentException("The 'property' expression must specify a property on the control.");

    var propertyInfo = memberExpression.Member as PropertyInfo;
    if (propertyInfo == null)
        throw new ArgumentException("The 'property' expression must specify a property on the control.");

    if (control.InvokeRequired)
        control.Invoke(
            (Action<C, Expression<Func<C, V>>, V>)SetPropertyInGuiThread,
            new object[] { control, property, value }
        );
    else
        propertyInfo.SetValue(control, value, null);
}

نسميها هكذا:

myButton.SetPropertyInGuiThread(b => b.Text, "Click Me!")
  1. يضيف الاختيار خالية إلى نتيجة "as MemberExpression".
  2. يحسن سلامة نوع ثابت.

خلاف ذلك ، فإن الأصل هو حل لطيف للغاية.


هذه هي الطريقة الكلاسيكية التي ينبغي عليك القيام بها:

using System;
using System.Windows.Forms;
using System.Threading;

namespace Test
{
    public partial class UIThread : Form
    {
        Worker worker;

        Thread workerThread;

        public UIThread()
        {
            InitializeComponent();

            worker = new Worker();
            worker.ProgressChanged += new EventHandler<ProgressChangedArgs>(OnWorkerProgressChanged);
            workerThread = new Thread(new ThreadStart(worker.StartWork));
            workerThread.Start();
        }

        private void OnWorkerProgressChanged(object sender, ProgressChangedArgs e)
        {
            // Cross thread - so you don't get the cross-threading exception
            if (this.InvokeRequired)
            {
                this.BeginInvoke((MethodInvoker)delegate
                {
                    OnWorkerProgressChanged(sender, e);
                });
                return;
            }

            // Change control
            this.label1.Text = e.Progress;
        }
    }

    public class Worker
    {
        public event EventHandler<ProgressChangedArgs> ProgressChanged;

        protected void OnProgressChanged(ProgressChangedArgs e)
        {
            if(ProgressChanged!=null)
            {
                ProgressChanged(this,e);
            }
        }

        public void StartWork()
        {
            Thread.Sleep(100);
            OnProgressChanged(new ProgressChangedArgs("Progress Changed"));
            Thread.Sleep(100);
        }
    }


    public class ProgressChangedArgs : EventArgs
    {
        public string Progress {get;private set;}
        public ProgressChangedArgs(string progress)
        {
            Progress = progress;
        }
    }
}

مؤشر ترابط العامل الخاص بك لديه حدث. يبدأ مؤشر ترابط واجهة المستخدم الخاص بك مؤشر ترابط آخر للقيام بهذا العمل وخطّاف هذا الحدث عامل بحيث يمكنك عرض حالة مؤشر ترابط العامل.

ثم في واجهة المستخدم تحتاج إلى عبور المواضيع لتغيير عنصر التحكم الفعلي ... مثل تسمية أو شريط التقدم.


يجب عليك التأكد من أن التحديث يحدث على مؤشر الترابط الصحيح؛ مؤشر ترابط واجهة المستخدم.

للقيام بذلك ، سيكون عليك استدعاء معالج الأحداث بدلاً من الاتصال به مباشرة.

يمكنك القيام بذلك عن طريق رفع الحدث الخاص بك مثل هذا:

(يتم كتابة الرمز هنا من رأسي ، لذلك لم أتحقق من التركيب الصحيح ، وما إلى ذلك ، ولكن يجب أن تجعلك تنطلق.)

if( MyEvent != null )
{
   Delegate[] eventHandlers = MyEvent.GetInvocationList();

   foreach( Delegate d in eventHandlers )
   {
      // Check whether the target of the delegate implements 
      // ISynchronizeInvoke (Winforms controls do), and see
      // if a context-switch is required.
      ISynchronizeInvoke target = d.Target as ISynchronizeInvoke;

      if( target != null && target.InvokeRequired )
      {
         target.Invoke (d, ... );
      }
      else
      {
          d.DynamicInvoke ( ... );
      }
   }
}

لاحظ أن التعليمات البرمجية أعلاه لن تعمل على مشاريع WPF ، لأن عناصر تحكم WPF لا تقوم بتطبيق واجهة ISynchronizeInvoke .

للتأكد من أن التعليمات البرمجية أعلاه تعمل مع Windows Forms و WPF ، وكافة الأنظمة الأساسية الأخرى ، يمكنك إلقاء نظرة على AsyncOperation AsyncOperationManager و AsyncOperationManager و SynchronizationContext .

من أجل رفع الأحداث بسهولة بهذه الطريقة ، قمت بإنشاء طريقة تمديد ، والتي تسمح لي بتبسيط رفع حدث من خلال الاتصال فقط:

MyEvent.Raise(this, EventArgs.Empty);

بالطبع ، يمكنك أيضًا الاستفادة من فئة BackGroundWorker ، والتي ستجرد هذه المسألة لك.


Create a class variable:

SynchronizationContext _context;

Set it in the constructor that creates your UI:

var _context = SynchronizationContext.Current;

When you want to update the label:

_context.Send(status =>{
    // UPDATE LABEL
}, null);

Even if the operation is time-consuming (thread.sleep in my example) - This code will NOT lock your UI:

 private void button1_Click(object sender, EventArgs e)
 {

      Thread t = new Thread(new ThreadStart(ThreadJob));
      t.IsBackground = true;
      t.Start();         
 }

 private void ThreadJob()
 {
     string newValue= "Hi";
     Thread.Sleep(2000); 

     this.Invoke((MethodInvoker)delegate
     {
         label1.Text = newValue; 
     });
 }

I just read the answers and this appears to be a very hot topic. I'm currently using .NET 3.5 SP1 and Windows Forms.

The well-known formula greatly described in the previous answers that makes use of the InvokeRequired property covers most of the cases, but not the entire pool.

What if the Handle has not been created yet?

The InvokeRequired property, as described here (Control.InvokeRequired Property reference to MSDN) returns true if the call was made from a thread that is not the GUI thread, false either if the call was made from the GUI thread, or if the Handle was not created yet.

You can come across an exception if you want to have a modal form shown and updated by another thread. Because you want that form shown modally, you could do the following:

private MyForm _gui;

public void StartToDoThings()
{
    _gui = new MyForm();
    Thread thread = new Thread(SomeDelegate);
    thread.Start();
    _gui.ShowDialog();
}

And the delegate can update a Label on the GUI:

private void SomeDelegate()
{
    // Operations that can take a variable amount of time, even no time
    //... then you update the GUI
    if(_gui.InvokeRequired)
        _gui.Invoke((Action)delegate { _gui.Label1.Text = "Done!"; });
    else
        _gui.Label1.Text = "Done!";
}

This can cause an InvalidOperationException if the operations before the label's update "take less time" (read it and interpret it as a simplification) than the time it takes for the GUI thread to create the Form 's Handle . This happens within the ShowDialog() method.

You should also check for the Handle like this:

private void SomeDelegate()
{
    // Operations that can take a variable amount of time, even no time
    //... then you update the GUI
    if(_gui.IsHandleCreated)  //  <---- ADDED
        if(_gui.InvokeRequired)
            _gui.Invoke((Action)delegate { _gui.Label1.Text = "Done!"; });
        else
            _gui.Label1.Text = "Done!";
}

You can handle the operation to perform if the Handle has not been created yet: You can just ignore the GUI update (like shown in the code above) or you can wait (more risky). This should answer the question.

Optional stuff: Personally I came up coding the following:

public class ThreadSafeGuiCommand
{
  private const int SLEEPING_STEP = 100;
  private readonly int _totalTimeout;
  private int _timeout;

  public ThreadSafeGuiCommand(int totalTimeout)
  {
    _totalTimeout = totalTimeout;
  }

  public void Execute(Form form, Action guiCommand)
  {
    _timeout = _totalTimeout;
    while (!form.IsHandleCreated)
    {
      if (_timeout <= 0) return;

      Thread.Sleep(SLEEPING_STEP);
      _timeout -= SLEEPING_STEP;
    }

    if (form.InvokeRequired)
      form.Invoke(guiCommand);
    else
      guiCommand();
  }
}

I feed my forms that get updated by another thread with an instance of this ThreadSafeGuiCommand , and I define methods that update the GUI (in my Form) like this:

public void SetLabeTextTo(string value)
{
  _threadSafeGuiCommand.Execute(this, delegate { Label1.Text = value; });
}

In this way I'm quite sure that I will have my GUI updated whatever thread will make the call, optionally waiting for a well-defined amount of time (the timeout).


I wanted to add a warning because I noticed that some of the simple solutions omit the InvokeRequired check.

I noticed that if your code executes before the window handle of the control has been created (eg before the form is shown), Invoke throws an exception. So I recommend always checking on InvokeRequired before calling Invoke or BeginInvoke .


Simply use something like this:

 this.Invoke((MethodInvoker)delegate
            {
                progressBar1.Value = e.ProgressPercentage; // runs on UI thread
            });

The easiest way I think:

   void Update()
   {
       BeginInvoke((Action)delegate()
       {
           //do your update
       });
   }

You may use the already-existing delegate Action :

private void UpdateMethod()
{
    if (InvokeRequired)
    {
        Invoke(new Action(UpdateMethod));
    }
}

You must use invoke and delegate

private delegate void MyLabelDelegate();
label1.Invoke( new MyLabelDelegate(){ label1.Text += 1; });




user-interface