Welcome to OStack Knowledge Sharing Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
643 views
in Technique[技术] by (71.8m points)

multithreading - C++11 threads to update MFC application windows. SendMessage(), PostMessage() required?

After spending a bit of time with simple UWP applications with C++/CX and ++/WinRT I have begun to enjoy some of the features of targeting that environment for Windows UI app development.

Now having to go back to a more familiar MFC application development I want to change my approach to something that is similar to UWP app development. The idea is to use asynchronous C++11 threads to generate content and modify content that is displayed in the MFC UI.

The main change I want to make is to use C++11 threads to offload some time-consuming tasks and have those threads communicate the results back to the main MFC UI.

Some of the tasks that I am looking to offload onto C++11 threads, which are similar to what I would use with asynchronous tasks with C++/CX and C++/WinRT in UWP apps are:

  • connect to and exchange data with another computer
  • open a data file and parse it to update the UI view
  • convert a data file to another format such as CSV and export to a file
  • read a file in a format such as CSV and convert the content into a data file
  • perform searches and filtering of the presentation of the data file in the UI

The problem I am running into is similar to the problem described in Can I have multiple GUI threads in MFC?, however, I am looking for a general approach rather than the specific progress bar update in that question.

I have been trying a simple test with an experimental MFC app using the Visual Studio template which has a tree control docked on the left to build the tree in a worker thread.

If I have a CViewTree, an MFC window that displays a tree view, which I want to update from a C++11 thread, I am currently using ::PostMessage() to request that the tree control in the docked pane is updated.

If I use a lambda with a global std::thread such as the following code:

std::thread t1;

void CClassView::FillClassView()
{
    // ::SendMessage() seems to deadlock so ::PostMessage() is required.
    t1 = std::thread([this]() { Sleep(5000);  ::PostMessage(this->m_hWnd, WM_COMMAND, ID_NEW_FOLDER, 0); });

}

the message handler for the MFC docked pane which looks like:

void CClassView::OnNewFolder()
{
    t1.join();   // this join seems to deadlock if ::SendMessage() is used.

    AddItemsToPane(m_wndClassView);
}

does indeed update the MFC docked pane with the tree control content just as if the function AddItemsToPane(m_wndClassView); were called at the same place where the C++11 thread is created. The pane update is delayed by 5 seconds when the C++11 thread is used just to provide a visible indication that the thread approach is actually working.

My problem is that I want the C++11 thread to create the content for the tree control and provide it to the docked pane rather than having the docked pane generate the content.

Thus far the only approach I can think of is to develop my own class library that will provide C++11 thread analogues to the MFC library and controls using ::PostMessage() to send the appropriate Windows messages to the designated MFC window or control.

I am wondering if it is possible to have the C++11 threads have their own, shadowing MFC control which they update and then send a message to the UI asking the UI to update its displayed control with the contents of the shadow MFC control? Or is there some other approach that people are using?

I am looking for some other, less arduous approaches to solving this problem of updating an MFC UI from C++11 threads.

By the way #1 ::SendMessage() appears to deadlock on the join() in CClassView::OnNewFolder() which I assume means that some kind of synchronization between the C+11 thread and the UI thread is blocking the C++11 thread from reaching its side of the join()?

Yes there is a deadlock as the thread waits for SendMessage() to return while the message handler is waiting at the join() for the thread to finish. According to the Windows Dev Center SendMessage function:

Sends the specified message to a window or windows. The SendMessage function calls the window procedure for the specified window and does not return until the window procedure has processed the message.

To send a message and return immediately, use the SendMessageCallback or SendNotifyMessage function. To post a message to a thread's message queue and return immediately, use the PostMessage or PostThreadMessage function.

By the way #2 It would also seem that using the actual Window handle rather than the this pointer in the lambda for the C++11 thread would be safer. Just in case the this pointer becomes undefined for some reason such as the control is removed?

By the way #3 The Microsoft concurrency namespace, provided via #include <ppltasks.h>, is an alternative to C++11 threads. The concurrency namespace functions are at a higher level of abstraction than C++11 threads and are easier to use.

For instance the above use of std:thread could be rewritten as:

void CClassView::FillClassView()
{
    concurrency::create_task([this]() { Sleep(5000);  ::SendMessage(this->m_hWnd, WM_COMMAND, ID_NEW_FOLDER, 0); });
}

and this does not require the use of a std::thread join() to cleanly terminate the thread. Also SendMessage() or PostMessage() may be used to send the Windows message since we do not have the same deadlock issue as we have with C++11 threads.

Notes

Note #1: About Messages and Message Queues as well as Using Messages and Message Queues.

For MFC specific content see Messages and Commands in the Framework.

Note #2: Multithreading with C++ and MFC and specifically Multithreading: Programming Tips which says.

If you have a multithreaded application that creates a thread in a way other than using a CWinThread object, you cannot access other MFC objects from that thread. In other words, if you want to access any MFC object from a secondary thread, you must create that thread with one of the methods described in Multithreading: Creating User-Interface Threads or Multithreading: Creating Worker Threads. These methods are the only ones that allow the class library to initialize the internal variables necessary to handle multithreaded applications.

Note #3: UWP APIs callable from a classic desktop app which says:

With some notable exceptions, the general rule is that a Universal Windows Platform (UWP) API can be called from a classic desktop app. The two main areas of APIs that are an exception to this general rule are XAML UI APIs, and APIs that require the calling app to have a package identity. UWP apps have a well-defined app model, and they have a package identity. Classic desktop apps do not have a well-defined app model, and they do not have a package identity. A classic desktop app that has been converted to a UWP app does have a package identity.

See as well the following blogs from Sep 2012 about WinRT with VS 2012 and Windows 8. Though C++/WinRT with VS 2017 seems more appropriate for Windows 10 than Windows Runtime template Library (WRL) used:

Note #4: MFC Desktop Applications which is a jumping off point with lots of links. See also MFC COM which is a jumping off point with lots of links about MFC with COM along with this article Introduction to COM. See as well MFC Macros and Globals.

As for using AfxGetMainWnd() to get the main application window, the Microsoft developer center has this to say in the article AfxGetMainWnd:

If AfxGetMainWnd is called from the application's primary thread, it returns the application's main window according to the above rules. If the function is called from a secondary thread in the application, the function returns the main window associated with the thread that made the call.

See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Answer

0 votes
by (71.8m points)

After some experimentation, there are a few recommendations that I feel comfortable making.

  • the concurrency task functionality is easier to use than C++11 std:thread and is more flexible in using with coroutines however std::async is easier to use than std::thread and works with co_await as well
  • coroutines using co_await look to be a great addition with concurrency when using C++/WinRT and the Async type functions in WinRT (see C++ Coroutines: Understanding operator co_await for a technical explanation)
  • you can make your own async functions using the concurrency::task<> template as the return type of the function or use concurrency::create_task() and you can use co_await with such a task
  • you can also use co_await with std::async() since std::async() returns a std::future<> which has an Awaitable interface (see await/yield: C++ coroutines though it is dated Nov-2016)
  • you can also use co_await with a std::future<> as provided by the get_future() method of a std::packaged_task<> ( see also What is the difference between packaged_task and async )
  • you can make generator functions using std::experimental::generator<type> as a function return type along with the co_yield operator to return a value of the specified type in the generated series
  • to update the MFC UI requires that any code runs in the MFC UI thread the MFC object was created so Windows messages are needed to communicate with the MFC windows and window class objects from other threads or you must switch the thread context/affinity to the UI thread context for that object
  • winrt::apartment_context can be used to capture the current thread context and later resumed using co_await which may be used to capture the main UI thread context to be reused later (see Programming with thread affinity in mind in the article Concurrency and asynchronous operations with C++/WinRT)
  • co_await winrt::resume_background(); can be used to push the current thread's context to a background thread which can be useful for a lengthy task that may be in the main UI thread context and you want to make sure that it is not
  • when sending messages to a window, make sure that the window actually has been created and exists, during application startup the application must create windows before you can use them; just because the MFC class exists does not mean the underlying window has been created yet
  • ::SendMessage() is synchronous in which the message is sent and a response is returned
  • ::PostMessage() is asynchronous in which the message is sent and a response is not returned
  • using ::PostMessage() be careful that pointers sent in messages do not go out of scope before the receiver uses them as there is typically a delay between when ::PostMessage() returns and the message handle receiving the message actually does something with the message
  • probably the most straightforward approach is to use the ON_MESSAGE() macro in the message map with a message handler interface of afx_msg LRESULT OnMessageThing(WPARAM, LPARAM)
  • you can use Windows message identifiers in the space beginning with the defined constant of WM_APP and the same identifier can be used in different classes
  • you can use much of C++/WinRT quite easily with MFC with just a bit of care though admittedly I only have tried a few things and there are some limitations such as not using XAML according to the documentation
  • if you do use C++/WinRT within an MFC application, you are limiting your application to versions of Windows that has the Windows Runtime which pretty much means Windows 10 (this rules out using C++/WinRT with Windows 7, POS Ready 7, etc.)
  • using C++/WinRT requires adding compiler option /stdc++17 to enable the ISO C++17 Standard for the C++ Language Standard and using coroutines requires the /await compiler option

Here are a view resources.

Microsoft Build 2018 Effective C++/WinRT for UWP and Win32 May 06, 2018 at 3:27PM by Brent Rector, Kenny Kerr

CppCon 2017: Scott Jones & Kenny Kerr C++/WinRT and the Future of C++ on Windows Published on Nov 2, 2017

Using Visual Studio 2017 Community edition, I created a new MFC Single Document Interface (SDI) project using the Visual Studio style. After the application comes up, it looks like the following image.

screen shot of MFC app in Visual Studio theme

Helper functions for messages

The first change I made was to provide a way to send a Windows message to one of the panes (ClassView or OutputWindow) that I would like to update. Since the CMainFrame class in MainFrm.h had the MFC objects for these windows as in:

protected:  // control bar embedded members
    CMFCMenuBar       m_wndMenuBar;
    CMFCToolBar       m_wndToolBar;
    CMFCStatusBar     m_wndStatusBar;
    CMFCToolBarImages m_UserImages;
    CFileView         m_wndFileView;
    CClassView        m_wndClassView;
    COutputWnd        m_wndOutput;
    CPropertiesWnd    m_wndProperties;

I modified the class to provide a way to send a message to these windows. I chose to use SendMessage() rather than PostMessage() to eliminate the pointer going out of scope issue. The concurrency class works fine with SendMessage().

LRESULT  SendMessageToFileView(UINT msgId, WPARAM wParam, LPARAM lParam) { return m_wndFileView.SendMessage(msgId, wParam, lParam); }
LRESULT  SendMessageToClassView(UINT msgId, WPARAM wParam, LPARAM lParam) { return m_wndClassView.SendMessage(msgId, wParam, lParam); }
LRESULT  SendMessageToOutputWnd(UINT msgId, WPARAM wParam, LPARAM lParam) { return m_wndOutput.SendMessage(msgId, wParam, lParam); }

These are the raw, bare infrastructure for sending the messages to update the various MFC windows. I put these into CMainFrame class as that is a central point and the AfxGetMainWnd() function allows me to access the object of that class at any place within the MFC application. Additional classes to wrap these raw functions would be appropriate.

I then put message handlers into each of the classes within the BEGIN_MESSAGE_MAP and the END_MESSAGE_MAP macros. The output window update was the easiest and simplest and look like:

BEGIN_MESSAGE_MAP(COutputWnd, CDockablePane)
    ON_WM_CREATE()
    ON_WM_SIZE()
    // ADD_ON: message handler for the WM_APP message containing an index as
    //         to which output window to write to along with a pointer to the
    //         text string to write.
    //         this->SendMessageToOutputWnd(WM_APP, COutputWnd::OutputBuild, (LPARAM)_T("some text"));
    ON_MESSAGE(WM_APP, OnAddItemsToPane)
END_MESSAGE_MAP()

with the message handler looking like:

// ADD_ON: message handler for the WM_APP message containing an array of the
//         struct ItemToInsert above. Uses method AddItemsToPane().
LRESULT  COutputWnd::OnAddItemsToPane(WPARAM wParam, LPARAM lParam)
{
    switch (wParam) {
    case OutputBuild:
        m_wndOutputBuild.AddString((TCHAR *)lParam);
        break;
    case OutputDebug:
        m_wndOutputDebug.AddString((TCHAR *)lParam);
        break;
    case OutputFind:
        m_wndOutputFind.AddString((TCHAR *)lParam);
        break;
    }

    return 0;
}

I added the method prototype to the class along with this enumeration to make the functionality a bit easier to use.

enum WindowList { OutputBuild = 1, OutputDebug = 2, OutputFind = 3 };

With the above changes I was able to insert into the message handler for "New" in BOOL CMFCAppWinRTDoc::OnNewDocument() the following code to put a text string into the "Build" Output Window:

CMainFrame *p = dynamic_cast <CMainFrame *> (AfxGetMainWnd());

if (p) {
    p->SendMessageToOutputWnd(WM_APP, COutputWnd::OutputBuild, (LPARAM)_T("this is a test from OnNewDocument()."));
}

Using C++/WinRT with MFC and concurrency

To test this along with testing using C++/WinRT with MFC, I added the following concurrency task to CMainFrame::OnCreate() which is called when the application is starting up. This source spins off a task which then uses the Syndication functionality of C++/WinRT to fetch an RSS feed list and displays the titles in the OutputWindow pane labeled "Build" as seen in the screen shot above.

concurrency::create_task([this]() {
    winrt::init_apartment();

    Sleep(5000);

    winrt::Windows::Foundation::Uri uri(L"http://kennykerr.ca/feed");
    winrt::Windows::Web::Syndication::SyndicationClient client;
    winrt::Windows::Web::Syndication::SyndicationFeed feed = client.RetrieveFeedAsync(uri).get();
    for (winrt::Windows::Web::Syndication::SyndicationItem item : feed.Items())
    {
        winrt::hstring title = item.Title().Text();
        this->SendMessageToOutputWnd(WM_APP, COutputWnd::OutputBuild, (LPARAM)title.c_str());  // print a string to an output window in the output pane.
    }
    winrt::uninit_apartment();
});

To use the concurrency and the C++/WinRT functionality I had to add a couple of include files near the top of MainFrm.c source file.

// ADD_ON: include files for using the concurrency namespace.
#include <experimental
esumable>
#include <pplawait.h>

#pragma comment(lib, "windowsapp")
#include "winrt/Windows.Foundation.h"
#include "winrt/Windows.Web.Syndication.h"

In addition, I had to modify the Properties of the solution to specify C++17 and an additional compiler option of /await which are marked with blue arrows in the screen shot below. screen shot of Visual Studio solution properties showing changes

Using co_await with MFC and C++/WinRT

From a helpful comment from @IInspectable I took a look at coroutines with Visual Studio 2017 and MFC. I have been curious about them however it seemed that I could not come up with anything that would


与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome to OStack Knowledge Sharing Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...