Search code examples
multithreadingwinformsc++-cliinvoke

Call a Form method from a different thread (Invoke)


I've got a WinForm running on my main thread and a while(true) loop running on a separate thread. Each loop of that while(true) creates a new System::String^ and I want to paste that String into a TextBox on my UI.

My file structure includes GUI.h, GUI.cpp, and Other.cpp.

GUI.h contains all the automatically created code for the main (and only) Form. It also has some Get, Set, and ButtonClick methods.

//GUI.h
#pragma once

#include <string>
#include <vector>
#include <cliext\vector>
#include <conio.h>
#include <list>
#include <iostream>

extern void BufferRecieveLoop();

namespace GUI_Example_Receive { 

    static bool loopFlag = true;

    using namespace System;
    using namespace System::ComponentModel;
    using namespace System::Collections;
    using namespace System::Windows::Forms;
    using namespace System::Data;
    using namespace System::Drawing;
    using namespace System::Threading;

    /// <summary>
    /// Summary for GUI
    /// </summary>
    public ref class GUI : public System::Windows::Forms::Form
    {
    public:
        GUI(void)
        {
            InitializeComponent();
        }
        std::vector<std::string> CollectText();
        void ContinueNormally(); // Object^);
        void DisableAllTextboxes();
        void EnableAllTextboxes();

    protected:
        /// <summary>
        /// Clean up any resources being used.
        /// </summary>
        ~GUI()
        {
            if (components)
            {
                delete components;
            }
        }

    private:
        //Labels
        //GroupBoxes
        //Buttons
        //SaveFile

    public:
        //TextBoxes
        System::Windows::Forms::TextBox^  consoleTextBox;

    private:
        System::ComponentModel::Container ^components;

#pragma region Windows Form Designer generated code
        void InitializeComponent(void)
        {
            //automatically made, lightly edited
        }
#pragma endregion

    public: 
        void SetConsoleTextBoxText(System::String^ input)
        {
            this->consoleTextBox->Text = input;
            this->consoleTextBox->Refresh();
        }

        void ClearConsoleTextBoxText()
        {
            this->consoleTextBox->Clear();
        }

        delegate void MyDelegate(System::String ^ str);

        void ClearAndSetConsoleTextBoxText(System::String ^ input)
        {
            /***************************************************
            if (InvokeRequired)
            {
                this->BeginInvoke(gcnew MyDelegate(this, &ClearAndSetConsoleTextBoxText), { input });
            }
            ***************************************************/
            ClearConsoleTextBoxText();
            SetConsoleTextBoxText(input);
        }

        System::Void startButton_Click(System::Object^  sender, System::EventArgs^  e)
        {
            loopFlag = true; //able to loop through ContinueNormally()

            ContinueNormally(); //method in GUI.cpp
        }

    };

    //https://social.msdn.microsoft.com/Forums/vstudio/en-US/4da834f0-d8f8-4abb-a655-ef9e99d51eb2/how-to-create-a-global-object-of-a-ref-class-type?forum=vcgeneral
    ref struct Globals {
        static GUI ^gui; //using Globals::gui everywhere to access the one Form
    };

}

Gui.cpp contains code to Run() the form, start a thread, and loop forever.

//GUI.cpp
void BufferRecieveLoop()
{
    while (true)
    {
        size_t bytes_read = multicast.Receive(buffer, Example::MTU_SIZE);

        incoming.Process(buffer, bytes_read, endian); //method in Other.cpp
    }
}

void GUI::ContinueNormally()
{
    System::Threading::Thread ^loopThread = gcnew System::Threading::Thread(gcnew System::Threading::ThreadStart(BufferRecieveLoop));
    loopThread->Start();
    loopThread->Join();
}

static void Start()
{
    Globals::gui = gcnew GUI;
    System::Windows::Forms::Application::Run(Globals::gui);
}

int __cdecl main(int argc, char* argv[])
{
    System::Windows::Forms::Application::EnableVisualStyles();
    System::Windows::Forms::Application::SetCompatibleTextRenderingDefault(false);

    Start();

    return 0;
}

Other.cpp creates a String^ and calls a method within GUI.h to change the text in a textbox.

//Other.cpp
void Process(const DIS::Pdu& packet)
{
    System::String^ sysStr2 = "stuff";

    GUI_Example_Receive::Globals::gui->ClearAndSetConsoleTextBoxText(sysStr2);

    //GUI_Example_Receive::Globals::gui->BeginInvoke(gcnew MyStringDelegate(GUI_Example_Receive::Globals::gui, &GUI_Example_Receive::GUI::ClearAndSetConsoleTextBoxText), { sysStr2 });
}

I don't know where to properly Invoke my methods. Nor do I know how to Invoke my methods. A lot of what I've found is C# and hasn't worked for me.

Do I invoke from Other.cpp or inside the method being called in GUI.h?


Solution

  • In case anyone else has issues with this in the future and, like me, doesn't find many c++ code examples, I'll post my solution.

    In my GUI.h file, I've got a method to SetConsoleTextBoxText(). This is usable only by the thread which owns consoleTextBox. Therefore, any other thread which trys to call that method will need to Invoke() the method (which is to relinquish control back to the owning thread).

    //GUI.h
    delegate void MyDelegate(System::String ^ text);
    
    void SetConsoleTextBoxText(System::String^ input)
    {
        if (this->consoleTextBox->InvokeRequired) //is a thread other than the owner trying to access?
        {
            MyDelegate^ myD = gcnew MyDelegate(this, &GUI::SetConsoleTextBoxText); 
            //GUI is the ref class. Replace with wherever your function is located.
            this->Invoke(myD, gcnew array<Object^> { input }); //Invoke the method recursively
        }
        else
        {
            //Normal function of this method. This will be hit after a recursive call or from the owning thread
            this->consoleTextBox->Text = input;
            this->consoleTextBox->Refresh();
        }
    }