Closed DinkydauSet closed 2 years ago
What I find difficult is where to keep the data for the opened windows. I want to create and open a window from a function like this:
void functionname()
{
json_form fm;
fm.show();
}
This creates the window and then immediately closes it, which makes sense because at the end of the function the local variable fm is destroyed.
My current solution to the problem is to create the window in a different thread and blocking the thread until the window is closed. A different thread automatically means a different instance of nana (I read somewhere), so the normal thing to do is to create a form and then call exec, which blocks the thread until all windows are closed:
thread([&, this]()
{
lock_guard<mutex> guard(helpwindow_mutex);
{
assert(exists == false);
exists = true;
helpform fm;
fm.caption("Help");
helpwindow = (window)fm; //window is a pointer type
fm.show();
exec();
exists = false;
helpwindow = nullptr;
}
}).detach();
This approach creates a new problem: when the main window is closed, nana's exec function returns, because the main window is the only window in its instance. Meanwhile there may still be a helpwindow open, but that window is from a different instance, so before main returns I have to make sure that all windows created like this are closed. I do that by storing the window pointers as global variables and calling API::close_window on those. That's still not enough, because that API function returns immediately after closing the window. It doesn't wait for the nana instance to end. That's why I use a mutex,. When the mutex lock is released, that means exec has returned and so the nana instances has ended. This works, so that's not a problem, but it becomes more complicated if I want to allow any number of windows to be open. Every window would need its own mutex, and where do I store those mutexes (as data) so that main can wait for them?
The solution: I put all this in the main_form class.
// This base class makes it possible to store pointers to all child windows in 1 vector, even if the windows are of different types.
class child_window_base {
public:
virtual ~child_window_base() {}
virtual void focus() = 0;
};
// The instances of this template are the actual child window classes. formtype must be a subclass of form
template <typename formtype>
class child_window : public child_window_base {
public:
formtype fm;
void focus() { fm.focus(); }
};
vector<shared_ptr<child_window_base>> childWindows;
/*
creates a childwindow of the specified type
The reason for PostMessage is this: I want the window to be removed from the vector of childWindows when it's destroyed. Doing so during the handling of the destroyed event causes a deadlock. I don't know why; nana must use some mutex during the event that is required by one of the destructors. The solution is to postpone cleanup of the resources until after the event has been handled, by sending a new message.
*/
template <typename formtype>
void spawn_child()
{
shared_ptr<child_window<formtype>> child = make_shared<child_window<formtype>>();
childWindows.push_back(child);
child_window_base* childpointer = child.get();
child->fm.events().destroy([childpointer]()
{
if(debug) cout << "child window is being closed" << endl;
PostMessage(main_form_hwnd, Message::CLEANUP_CHILD_WINDOW, reinterpret_cast<WPARAM>(childpointer), 0);
if(debug) cout << "child window closed" << endl;
});
//type specific things
if constexpr( is_same<formtype, json_form>::value )
{
json_form& json = child->fm;
json.capture.events().click([this, &json](const arg_click& arg)
{
FractalCanvas* canvas = activeCanvas();
if (canvas != nullptr) {
json.text.caption( canvas->P().toJson() );
}
else {
msgbox mb(json, "Error", msgbox::ok);
mb.icon(mb.icon_error);
mb << "No fractal tab open";
mb.show();
}
});
json.apply.events().click([this, &json](const arg_click& arg)
{
FractalCanvas* canvas = activeCanvas();
if (canvas != nullptr) {
//try to apply the changes to a copy first
FractalParameters P = canvas->P();
string textContent = json.text.caption();
bool success = P.fromJson(textContent);
if (success) {
canvas->changeParameters(P, EventSource::JSON);
}
else {
msgbox mb(json, "Error", msgbox::ok);
mb.icon(mb.icon_error);
mb << "Invalid JSON";
mb.show();
}
}
else {
msgbox mb(json, "Error", msgbox::ok);
mb.icon(mb.icon_error);
mb << "No fractal tab open";
mb.show();
}
});
}
child->fm.show();
}
// Creates a childwindow of the specified type if none exist yet, otherwise focuses the existing one
template <typename formtype>
void spawn_unique_child()
{
for (int i=0; i<childWindows.size(); i++)
{
child_window<formtype>* child = dynamic_cast<child_window<formtype>*>(childWindows[i].get());
if (child != nullptr)
{
// This type of window already exists. Focus it:
child->focus();
return;
}
}
//This type of windows does not exist yet. Create one:
spawn_child<formtype>();
}
It's tricks with templates and polymorphism to achieve something that I want to do which is not normally possible:
I can't just make every window type a subclass of a common base class because I don't have control over nanas classes, but what I can do is create wrapper classes for each window type which ARE subclasses of a common base class. (I saw some people on the internet recommend against this approach because it's an anti-pattern or something but it works so what's the problem??) The polymorphism of c++ automatically does what I could program myself but fortunately I don't have to: store an indicator of the actual type for each base class pointer, and when destructing the resource, calling the right destructor based on the type indicator. For this, it's important that the destructor of the base class is virtual.
spawn_child(json_form);
Here I pass a type as a function parameter, but that's not something that exists in c++ so I solved the problem by making the function a template, so that I could instead do:
spawn_child<json_form>();
The cleanup goes like this:
sc.make_before(Message::CLEANUP_CHILD_WINDOW, [&fm](UINT, WPARAM wParam, LPARAM, LRESULT*)
{
if(debug) cout << "Message CLEANUP_CHILD_WINDOW" << endl;
main_form::child_window_base* child = reinterpret_cast<main_form::child_window_base*>(wParam);
int indexof = -1;
for (int i=0; i<fm.childWindows.size(); i++) {
if (fm.childWindows[i].get() == child) {
indexof = i;
break;
}
}
// There is a valid situation in which indexof remains -1. This happens when a window is closed BY removing its resource from childWindows (which happens in the destroy event handler below). That also triggers the cleanup, but there is nothing to clean up.
if (indexof >= 0)
fm.childWindows.erase(fm.childWindows.begin() + indexof);
return false;
});
A window is identified by its memory address. When the window is in the vector (note that this may not always be the case - in other words, the cleanup is not always needed) it's removed from the vector. This frees all resources.
Actually the only thing that has to be done to close a window is to destruct the resources. That's how nana works, apparently, which is nice. To close all child windows when the program has to close, I just remove every element from the vector of child windows:
fm.events().destroy([&fm]()
{
if(debug) cout << "main form is being destroyed" << endl;
fm.childWindows.clear(); //this closes all child windows
if(debug) cout << "all child windows closed" << endl;
});
How to manage multiple windows? They should all be closed when the main window is closed. The program should not crash while closing.
Relevant facts: