11
\$\begingroup\$

I recently started work on a game project, and decided to use GLFW for windowing / OpenGL context handling. GLFW doesn't have support for creating message boxes / dialogs, and I was unable to find anything simple online to do the job, so I decided to take a shot at creating a library to do so.

I've completed a first iteration of the library (which I'm calling 'Boxer') that supports the creation of message boxes on OS X, Windows, and Linux (with GTK+). I've never released any code intended for reuse, and I'd love to get some feedback, specifically in C++ / library best practices.

There are four files: the header file and an implementation file for each OS. Everything is set up to be built with CMake.

The project is available on my GitHub.

boxer.h

#ifndef BOXER_H
#define BOXER_H

namespace boxer {

enum class Style {
   Info,
   Warning,
   Error,
   Question
};

enum class Buttons {
   OK,
   OKCancel,
   YesNo
};

enum class Selection {
   OK,
   Cancel,
   Yes,
   No,
   None
};

const Style DEFAULT_STYLE = Style::Info;
const Buttons DEFAULT_BUTTONS = Buttons::OK;

Selection show(const char *message, const char *title, Style style, Buttons buttons);

inline Selection show(const char *message, const char *title, Style style) {
   return show(message, title, style, DEFAULT_BUTTONS);
}

inline Selection show(const char *message, const char *title, Buttons buttons) {
   return show(message, title, DEFAULT_STYLE, buttons);
}

inline Selection show(const char *message, const char *title) {
   return show(message, title, DEFAULT_STYLE, DEFAULT_BUTTONS);
}

} // namespace boxer

#endif

boxer_linux.cpp

#include <boxer/boxer.h>
#include <gtk/gtk.h>

namespace boxer {

namespace {

GtkMessageType getMessageType(Style style) {
   switch (style) {
      case Style::Info:
         return GTK_MESSAGE_INFO;
      case Style::Warning:
         return GTK_MESSAGE_WARNING;
      case Style::Error:
         return GTK_MESSAGE_ERROR;
      case Style::Question:
         return GTK_MESSAGE_QUESTION;
      default:
         return GTK_MESSAGE_INFO;
   }
}

GtkButtonsType getButtonsType(Buttons buttons) {
   switch (buttons) {
      case Buttons::OK:
         return GTK_BUTTONS_OK;
      case Buttons::OKCancel:
         return GTK_BUTTONS_OK_CANCEL;
      case Buttons::YesNo:
         return GTK_BUTTONS_YES_NO;
      default:
         return GTK_BUTTONS_OK;
   }
}

Selection getSelection(gint response) {
   switch (response) {
      case GTK_RESPONSE_OK:
         return Selection::OK;
      case GTK_RESPONSE_CANCEL:
         return Selection::Cancel;
      case GTK_RESPONSE_YES:
         return Selection::Yes;
      case GTK_RESPONSE_NO:
         return Selection::No;
      default:
         return Selection::None;
   }
}

} // namespace

Selection show(const char *message, const char *title, Style style, Buttons buttons) {
   if (!gtk_init_check(0, NULL)) {
      return Selection::None;
   }

   GtkWidget *dialog = gtk_message_dialog_new(NULL,
                                              GTK_DIALOG_MODAL,
                                              getMessageType(style),
                                              getButtonsType(buttons),
                                              "%s",
                                              message);
   gtk_window_set_title(GTK_WINDOW(dialog), title);
   Selection selection = getSelection(gtk_dialog_run(GTK_DIALOG(dialog)));

   gtk_widget_destroy(GTK_WIDGET(dialog));
   while (g_main_context_iteration(NULL, false));

   return selection;
}

} // namespace boxer

boxer_osx.mm

#include <boxer/boxer.h>
#import <Cocoa/Cocoa.h>

namespace boxer {

namespace {

NSString* const OK_STR = @"OK";
NSString* const CANCEL_STR = @"Cancel";
NSString* const YES_STR = @"Yes";
NSString* const NO_STR = @"No";

NSAlertStyle getAlertStyle(Style style) {
   switch (style) {
      case Style::Info:
         return NSInformationalAlertStyle;
      case Style::Warning:
         return NSWarningAlertStyle;
      case Style::Error:
         return NSCriticalAlertStyle;
      case Style::Question:
         return NSWarningAlertStyle;
      default:
         return NSInformationalAlertStyle;
   }
}

void setButtons(NSAlert *alert, Buttons buttons) {
   switch (buttons) {
      case Buttons::OK:
         [alert addButtonWithTitle:OK_STR];
         break;
      case Buttons::OKCancel:
         [alert addButtonWithTitle:OK_STR];
         [alert addButtonWithTitle:CANCEL_STR];
         break;
      case Buttons::YesNo:
         [alert addButtonWithTitle:YES_STR];
         [alert addButtonWithTitle:NO_STR];
         break;
      default:
         [alert addButtonWithTitle:OK_STR];
   }
}

Selection getSelection(int index, Buttons buttons) {
   switch (buttons) {
      case Buttons::OK:
         return index == NSAlertFirstButtonReturn ? Selection::OK : Selection::None;
      case Buttons::OKCancel:
         if (index == NSAlertFirstButtonReturn) {
            return Selection::OK;
         } else if (index == NSAlertSecondButtonReturn) {
            return Selection::Cancel;
         } else {
            return Selection::None;
         }
      case Buttons::YesNo:
         if (index == NSAlertFirstButtonReturn) {
            return Selection::Yes;
         } else if (index == NSAlertSecondButtonReturn) {
            return Selection::No;
         } else {
            return Selection::None;
         }
      default:
         return Selection::None;
   }
}

} // namespace

Selection show(const char *message, const char *title, Style style, Buttons buttons) {
   NSAlert *alert = [[NSAlert alloc] init];

   [alert setMessageText:[NSString stringWithCString:title
                                   encoding:[NSString defaultCStringEncoding]]];
   [alert setInformativeText:[NSString stringWithCString:message
                                       encoding:[NSString defaultCStringEncoding]]];

   [alert setAlertStyle:getAlertStyle(style)];
   setButtons(alert, buttons);

   Selection selection = getSelection([alert runModal], buttons);
   [alert release];

   return selection;
}

} // namespace boxer

boxer_win.cpp

#include <boxer/boxer.h>
#include <windows.h>

namespace boxer {

namespace {

UINT getIcon(Style style) {
   switch (style) {
      case Style::Info:
         return MB_ICONINFORMATION;
      case Style::Warning:
         return MB_ICONWARNING;
      case Style::Error:
         return MB_ICONERROR;
      case Style::Question:
         return MB_ICONQUESTION;
      default:
         return MB_ICONINFORMATION;
   }
}

UINT getButtons(Buttons buttons) {
   switch (buttons) {
      case Buttons::OK:
         return MB_OK;
      case Buttons::OKCancel:
         return MB_OKCANCEL;
      case Buttons::YesNo:
         return MB_YESNO;
      default:
         return MB_OK;
   }
}

Selection getSelection(int response) {
   switch (response) {
      case IDOK:
         return Selection::OK;
      case IDCANCEL:
         return Selection::Cancel;
      case IDYES:
         return Selection::Yes;
      case IDNO:
         return Selection::No;
      default:
         return Selection::None;
   }
}

} // namespace

Selection show(const char *message, const char *title, Style style, Buttons buttons) {
   UINT flags = MB_TASKMODAL;

   flags |= getIcon(style);
   flags |= getButtons(buttons);

   return getSelection(MessageBox(NULL, message, title, flags));
}

} // namespace boxer

Example

Here is an example program that uses Boxer:

#include <boxer/boxer.h>

int main(int argc, char *argv[]) {
   boxer::show("Simple message boxes are very easy to create.", "Simple Example");

   boxer::show("There are a few different message box styles to choose from.", "Style Example", boxer::Style::Error);

   boxer::Selection selection;
   do {
      selection = boxer::show("Different buttons may be used, and the user's selection can be checked. Would you like to see this message again?", "Selection Example", boxer::Style::Question, boxer::Buttons::YesNo);
   } while (selection == boxer::Selection::Yes);

   return 0;
}
\$\endgroup\$
  • \$\begingroup\$ That's a nice little library. Do you have it available somewhere? If not, I recommend creating a repository on GitHub or Bitbucket. \$\endgroup\$ – glampert Dec 22 '14 at 14:35
  • 1
    \$\begingroup\$ I have it on my GitHub - I just edited my post to include the link at the bottom. \$\endgroup\$ – Samusaaron3 Dec 22 '14 at 17:28
  • \$\begingroup\$ on sourceforge, tiny file dialogs has function calls for many kind of dialogs. it's a simple C C++ cross-platform file to add to your project. \$\endgroup\$ – tinyfiledialogs Jul 4 '16 at 10:25
9
+100
\$\begingroup\$

Documentation

As this is a library I'm missing documentation for the header file. At the very least it should say that it is a modal dialogue (is it in all cases?) and what error scenarios there are.

Export signatures

You're missing the __declspec(dllexport)/__declspec(dllimport) and equivalents for Linux/MacOS if you're going to pass this of as a library,

Features

For the library to be useful I would like some more dialogue types. For example I would like to be able to give user defined buttons as an array and get the clicked button's index back. I would also like a text entry field type dialogue for quick 'n dirty user input. Possibly with a callback to validate the input and re-try the dialogue.

Error handling

I do not like the fact that error to initialize GTK cannot be distinguished from the user pressing the cross without making a choice. I would rather it threw an exception which I can choose to handle or not.

C API

There is nothing in your API that requires C++ (sure you use some C++ features like enums but they aren't technically required here). As it is a library, making the API in C (with appropriate #ifdef __cpluplus extern "C" thinggies) makes it more widely usable.

The next part I'm not 100% sure of but I do recall that the C++ ABI isn't stable for Visual C++. Hence to link with a C++ .lib/.dll you need to compile the lib/dll with the same version of Visual Studio. Obviously if you only ship source code, there's no problem. But the C ABI is stable so a C library wouldn't have any issue there.

\$\endgroup\$
5
\$\begingroup\$

This is not much of a code review. I think your code is great and I don't have any suggested changes at the moment, but I'd still like to share this bit of information with you, which was hard to compile, due the lack of official documentation.

If by any chance you aren't very happy about mixing Objective-C with a C++ project, there is a way of accessing the underlaying message dialog API with C++, using CoreFoundation.

Documentation is very sparse, or plain inexistent, but the following example compiles without warnings, so it appears that the API is fully supported and not deprecated as of OSX 10.10.4:

#include <CoreFoundation/CoreFoundation.h>

// Icons to decorate the dialog message box.
// Use `Icon::None` for no icon decoration.
struct Icon
{
    enum Enum
    {
        None,
        Information,
        Warning,
        Error
    };
};

// Buttons present in the dialog message box.
// The clicked button is returned by `showMessageBox()`.
// These can be combined as flags in the `buttons` parameter.
struct Button
{
    enum Enum
    {
        Ok     = 1 << 0, // Has an OK/accept button
        Cancel = 1 << 1  // Has a cancel button
    };
};

Button::Enum showMessageBox(Icon::Enum icon, unsigned int buttons, const char * title, const char * message)
{
    //
    // Similar question pertaining `CFUserNotificationDisplayAlert()` and iOS:
    //   http://stackoverflow.com/questions/2199642/display-cfusernotificationdisplayalert-on-iphone-lock-screen
    //

    // Provide defaults in case of null:
    if (title   == nullptr) { title   = "(null)"; }
    if (message == nullptr) { message = "???";    }

    CFOptionFlags cfAlertIcon;
    switch (icon)
    {
    case Icon::Information :
        cfAlertIcon = kCFUserNotificationNoteAlertLevel;
        break;

    case Icon::Warning :
        cfAlertIcon = kCFUserNotificationCautionAlertLevel;
        break;

    case Icon::Error :
        cfAlertIcon = kCFUserNotificationStopAlertLevel;
        break;

    default : // Icon::None
        cfAlertIcon = kCFUserNotificationPlainAlertLevel;
        break;
    } // switch (icon)

    // Convert button flags to CF strings:
    CFStringRef cfStrDefaultButton   = nullptr;
    CFStringRef cfStrAlternateButton = nullptr;
    bool isDefaultBtnCancel = false;

    if ((buttons & Button::Ok) && (buttons & Button::Cancel))
    {
        cfStrDefaultButton   = CFSTR("OK");
        cfStrAlternateButton = CFSTR("Cancel");
    }
    else if (buttons & Button::Ok)
    {
        cfStrDefaultButton = CFSTR("OK");
    }
    else if (buttons & Button::Cancel)
    {
        cfStrDefaultButton = CFSTR("Cancel");
        isDefaultBtnCancel = true;
    }
    else
    {
        // Bad flags, display the OK button anyway.
        cfStrDefaultButton = CFSTR("OK");
    }

    CFStringRef cfStrHeader  = CFStringCreateWithCString(kCFAllocatorDefault, title,   kCFStringEncodingUTF8);
    CFStringRef cfStrMessage = CFStringCreateWithCString(kCFAllocatorDefault, message, kCFStringEncodingUTF8);

    // Launch the message box, blocking the current thread
    // until the user click a button or closes the dialog.
    CFOptionFlags result;
    CFUserNotificationDisplayAlert(
        0,                    // No timeout
        cfAlertIcon,          // Icon of the message box
        nullptr,              // Icon URL if loading from web
        nullptr,              // Second URL ???
        nullptr,              // Localized strings
        cfStrHeader,          // Header text
        cfStrMessage,         // Message text
        cfStrDefaultButton,   // Default button
        cfStrAlternateButton, // Second button
        nullptr,              // Third button, null -> no other button
        &result               // Response flags
    );

    CFRelease(cfStrHeader);
    CFRelease(cfStrMessage);

    if (result == kCFUserNotificationDefaultResponse)
    {
        return isDefaultBtnCancel ? Button::Cancel : Button::Ok;
    }

    // kCFUserNotificationAlternateResponse
    return Button::Cancel;
}

A simple driver:

#include <iostream>
int main()
{
    std::cout << "Hello\n";
    showMessageBox(Icon::Information, Button::Ok, "Hello", "This is a test");
    std::cout << "Bye\n";
}

Functionality should be equivalent to the Cocoa version you have. The advantage is keeping everything C++, if you care about that. The key point in the above is CFUserNotificationDisplayAlert(), which is the MessageBox() equivalent of OSX.

You can test this by compiling with:

$ clang++ -std=c++11 -Wall -Wextra -framework CoreFoundation osx_messagebox.cpp
\$\endgroup\$
  • \$\begingroup\$ Thanks @glampert for chipping in, if I could, I would have split the bounty in two... \$\endgroup\$ – janos Jul 10 '15 at 21:31

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.