Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why am I allowed to load an image in the non-GUI thread?

Well known is that you cannot change the GUI with any other thread than the GUI thread. So a simple trick that is commonly used (which I use) is invoking:

this.Invoke((MethodInvoker)delegate { pictureBox1.Visible = false; });

I was building my program and started it, quickly noticing that I forgot to put an PictureBox.Load(string url) in the invoker, however no error had occurred.

So I am curious, why am I not allowed to do (in non-GUI thread):

pictureBox1.Visible = false; // eg.

But am allowed to do:

pictureBox1.Load(url); // url = link to image
like image 999
Thomas Wagenaar Avatar asked Mar 15 '23 10:03

Thomas Wagenaar


2 Answers

When you load a new image, this is what happens inside the PictureBox (this is called from the Load method):

    private void InstallNewImage(Image value,
                                 ImageInstallationType installationType)
    {
        StopAnimate();
        this.image = value;

        LayoutTransaction.DoLayoutIf(AutoSize, this, this, PropertyNames.Image); 

        Animate();
        if (installationType != ImageInstallationType.ErrorOrInitial)
        {
            AdjustSize();
        }
        this.imageInstallationType = installationType;

        Invalidate();
        CommonProperties.xClearPreferredSizeCache(this);
    }

So you can see that all it really does is set the image and then call Invalidate, which can be called from other threads.

Visible (inherited from Control), which you can see here, does a lot of stuff through p/invoke and must be done on the main UI thread.

    protected virtual void SetVisibleCore(bool value) {
        try {
            System.Internal.HandleCollector.SuspendCollect();

            if (GetVisibleCore() != value) {
                if (!value) {
                    SelectNextIfFocused();
                }

                bool fireChange = false;

                if (GetTopLevel()) {

                    // The processing of WmShowWindow will set the visibility
                    // bit and call CreateControl()
                    //
                    if (IsHandleCreated || value) {
                            SafeNativeMethods.ShowWindow(new HandleRef(this, Handle), value ? ShowParams : NativeMethods.SW_HIDE);
                    }
                }
                else if (IsHandleCreated || value && parent != null && parent.Created) {

                    // We want to mark the control as visible so that CreateControl
                    // knows that we are going to be displayed... however in case
                    // an exception is thrown, we need to back the change out.
                    //
                    SetState(STATE_VISIBLE, value);
                    fireChange = true;
                    try {
                        if (value) CreateControl();
                        SafeNativeMethods.SetWindowPos(new HandleRef(window, Handle),
                                                       NativeMethods.NullHandleRef,
                                                       0, 0, 0, 0,
                                                       NativeMethods.SWP_NOSIZE
                                                       | NativeMethods.SWP_NOMOVE
                                                       | NativeMethods.SWP_NOZORDER
                                                       | NativeMethods.SWP_NOACTIVATE
                                                       | (value ? NativeMethods.SWP_SHOWWINDOW : NativeMethods.SWP_HIDEWINDOW));
                    }
                    catch {
                        SetState(STATE_VISIBLE, !value);
                        throw;
                    }
                }
                if (GetVisibleCore() != value) {
                    SetState(STATE_VISIBLE, value);
                    fireChange = true;
                }

                if (fireChange) {
                    // We do not do this in the OnPropertyChanged event for visible
                    // Lots of things could cause us to become visible, including a
                    // parent window.  We do not want to indescriminiately layout
                    // due to this, but we do want to layout if the user changed
                    // our visibility.
                    //

                    using (new LayoutTransaction(parent, this, PropertyNames.Visible)) {
                        OnVisibleChanged(EventArgs.Empty);
                    }
                }
                UpdateRoot();
            }
            else { // value of Visible property not changed, but raw bit may have

                if (!GetState(STATE_VISIBLE) && !value && IsHandleCreated) {
                    // PERF - setting Visible=false twice can get us into this else block
                    // which makes us process WM_WINDOWPOS* messages - make sure we've already 
                    // visible=false - if not, make it so.
                     if (!SafeNativeMethods.IsWindowVisible(new HandleRef(this,this.Handle))) {
                        // we're already invisible - bail.
                        return;
                     }
                }

                SetState(STATE_VISIBLE, value);

                // If the handle is already created, we need to update the window style.
                // This situation occurs when the parent control is not currently visible,
                // but the child control has already been created.
                //
                if (IsHandleCreated) {

                    SafeNativeMethods.SetWindowPos(
                                                      new HandleRef(window, Handle), NativeMethods.NullHandleRef, 0, 0, 0, 0, NativeMethods.SWP_NOSIZE |
                                                      NativeMethods.SWP_NOMOVE | NativeMethods.SWP_NOZORDER | NativeMethods.SWP_NOACTIVATE |
                                                      (value ? NativeMethods.SWP_SHOWWINDOW : NativeMethods.SWP_HIDEWINDOW));
                }
            }
        }
        finally {
            System.Internal.HandleCollector.ResumeCollect();
        }
    }

Just as a side-note, a lot of these "what happens under the hood" can be figured out by browsing the reference source, or creating a small application and decompiling it (I would recommend JetBrains DotPeek, or ildasm works too).

like image 193
Ron Beyer Avatar answered Mar 17 '23 19:03

Ron Beyer


The reason you do not get the "Cross-thread operation not valid" message is because the exception is thrown when the control Handle property is accessed:

public IntPtr Handle {
    get {
        if (checkForIllegalCrossThreadCalls &&
            !inCrossThreadSafeCall &&
            InvokeRequired) {
            throw new InvalidOperationException(SR.GetString(SR.IllegalCrossThreadCall,
                                                             Name));
        }

        if (!IsHandleCreated)
        {
            CreateHandle();
        }

        return HandleInternal;
    }
}

As you can see from the accepted answer, SetVisibleCore (called by Visible) uses Handle several times, but InstallNewImage (used by Load) does not. (Actually, Invalidate does access the Handle property, but it does so within a thread safe call scope which is fine.)

like image 20
Andy West Avatar answered Mar 17 '23 18:03

Andy West