From 727bd94560ca9056cdbe40fe2e7923ed008b6eac Mon Sep 17 00:00:00 2001 From: Albrecht Schlosser Date: Mon, 20 Nov 2023 20:12:02 +0100 Subject: [PATCH] Add commandline conversion for Windows (no-op on other platforms) - add Fl::args_to_utf8() to convert commandline arguments to UTF-8 This new function closes the gap that previously only Visual Studio applications converted their commandlines to UTF-8. Tested with MinGW, MSYS2/MinGW-w64, and Visual Studio (2019). --- FL/Fl.H | 3 + examples/howto-parse-args.cxx | 27 +++++-- src/CMakeLists.txt | 9 ++- src/Fl.cxx | 73 +++++++++++++++++++ src/Fl_System_Driver.H | 10 ++- src/drivers/WinAPI/Fl_WinAPI_System_Driver.H | 4 + .../WinAPI/Fl_WinAPI_System_Driver.cxx | 34 +++++++++ src/fl_call_main.c | 55 +++++++++----- test/pixmap_browser.cxx | 5 +- 9 files changed, 189 insertions(+), 31 deletions(-) diff --git a/FL/Fl.H b/FL/Fl.H index b52c33c72..3757b65be 100644 --- a/FL/Fl.H +++ b/FL/Fl.H @@ -1398,6 +1398,9 @@ public: static int system(const char *command); + // Convert Windows commandline arguments to UTF-8 (documented in src/Fl.cxx) + static int args_to_utf8(int argc, char ** &argv); + #ifdef FLTK_HAVE_CAIRO /** \defgroup group_cairo Cairo Support Functions and Classes @{ diff --git a/examples/howto-parse-args.cxx b/examples/howto-parse-args.cxx index 393344128..ef756d09b 100644 --- a/examples/howto-parse-args.cxx +++ b/examples/howto-parse-args.cxx @@ -11,7 +11,16 @@ // usual *nix idiom of "option=value", and provides no validation nor // conversion of the parameter string into ints or floats. // -// Copyright 1998-2020 by Bill Spitzak and others. +// Example 1: +// +// ./howto-parse-args -ti "FLTK is great" -o "FLTK is a great GUI tool" +// +// Example 2: translated to Japanese and simplified Chinese, respectively, +// by a well known internet translation service. +// +// ./howto-parse-args -ti "FLTKは素晴らしいです" -o "FLTK 是一个很棒的 GUI 工具" +// +// Copyright 1998-2023 by Bill Spitzak and others. // // This library is free software. Distribution and use rights are outlined in // the file "COPYING" which should have been included with this file. If this @@ -42,13 +51,14 @@ char *optionString = 0; * returns 1 if argv[i] matches on its own, * returns 0 if argv[i] does not match. */ -int arg(int argc, char **argv, int &i) -{ +int arg(int argc, char **argv, int &i) { + if (strcmp("-h", argv[i]) == 0 || strcmp("--help", argv[i]) == 0) { helpFlag = 1; i += 1; return 1; } + if (strcmp("-o", argv[i]) == 0 || strcmp("--option", argv[i]) == 0) { if (i < argc-1 && argv[i+1] != 0) { optionString = argv[i+1]; @@ -59,8 +69,13 @@ int arg(int argc, char **argv, int &i) return 0; } -int main(int argc, char** argv) -{ +int main(int argc, char** argv) { + + // Convert commandline arguments in 'argv' to UTF-8 on Windows. + // This is a no-op on all other platforms (see documentation). + + Fl::args_to_utf8(argc, argv); + int i = 1; if (Fl::args(argc, argv, i, arg) < argc) // note the concatenated strings to give a single format string! @@ -84,6 +99,8 @@ int main(int argc, char** argv) textBox->label(optionString); else textBox->label("re-run with [-o|--option] text"); + + mainWin->resizable(mainWin); mainWin->show(argc, argv); return Fl::run(); } diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 393e06f6e..a32319889 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -578,9 +578,14 @@ list (APPEND SHARED_FILES ${HEADER_FILES} ${DRIVER_HEADER_FILES}) set (STATIC_FILES ${SHARED_FILES}) -if (MSVC) +# Visual Studio (MSVC) is known to need WinMain() and maybe BORLAND +# needs it as well, hence we include it on all Windows platforms. +# The GNU compilers (MinGW, MSYS2, Cygwin) disable compilation inside +# the source file which is what we finally want and need. + +if (WIN32) list (APPEND STATIC_FILES fl_call_main.c) -endif (MSVC) +endif () ####################################################################### diff --git a/src/Fl.cxx b/src/Fl.cxx index 105bc018b..013d4dee1 100644 --- a/src/Fl.cxx +++ b/src/Fl.cxx @@ -2281,3 +2281,76 @@ FL_EXPORT const char* fl_local_shift = Fl::system_driver()->shift_name(); FL_EXPORT const char* fl_local_meta = Fl::system_driver()->meta_name(); FL_EXPORT const char* fl_local_alt = Fl::system_driver()->alt_name(); FL_EXPORT const char* fl_local_ctrl = Fl::system_driver()->control_name(); + +/** + Convert Windows commandline arguments to UTF-8. + + \note This function does nothing on other (non-Windows) platforms, hence + you may call it on all platforms or only on Windows by using platform + specific code like '\#ifdef _WIN32' etc. - it's your choice. + Calling it on other platforms returns quickly w/o wasting much CPU time. + + This function must be called on Windows platforms in \c main() + before the array \c argv is used if your program uses any commandline + argument strings (these should be UTF-8 encoded). + This applies also to standard FLTK commandline arguments like + "-name" (class name) and "-title" (window title in the title bar). + + Unfortunately Windows \b neither provides commandline arguments in UTF-8 + encoding \b nor as Windows "Wide Character" strings in the standard + \c main() and/or the Windows specific \c WinMain() function. + + On Windows platforms (no matter which build system) this function calls + a Windows specific function to retrieve commandline arguments as Windows + "Wide Character" strings, converts these strings to an internally allocated + buffer (or multiple buffers) and returns the result in \c argv. + For implementation details please refer to the source code; however these + details may be changed in the future. + + Note that \c argv is provided by reference so it can be overwritten. + + In the recommended simple form the function overwrites the variable + \c argv and allocates a new array of strings pointed to by \c argv. + You may use this form on all platforms and it is as simple as adding + one line to old programs to make them work with international (UTF-8) + commandline arguments. + + \code + int main(int argc, char **argv) { + Fl::args_to_utf8(argc, argv); // add this line + // ... use argc and argv, e.g. for commandline parsing + window->show(argc, argv); + return Fl::run(); + } + \endcode + + For an example see 'examples/howto-parse-args.cxx' in the FLTK sources. + + If you want to retain the original \c argc and \c argv variables the + following slightly longer and more complicated code works as well on + all platforms. + + \code + int main(int argc, char **argv) { + char **argvn = argv; // must copy argv to work on all platforms + int argcn = Fl::args_to_utf8(argc, argvn); + // ... use argcn and argvn, e.g. for commandline parsing + window->show(argcn, argvn); + return Fl::run(); + } + \endcode + + \param[in] argc used only on non-Windows platforms + \param[out] argv modified only on Windows platforms + \returns argument count (always the same as argc) + + \since 1.4.0 + + \internal This function must not open the display, otherwise + commandline processing (e.g. by fluid) would open the display. + OTOH calling it when the display is opened wouldn't work either + for the same reasons ('fluid -c' doesn't open the display). +*/ +int Fl::args_to_utf8(int argc, char ** &argv) { + return Fl::system_driver()->args_to_utf8(argc, argv); +} diff --git a/src/Fl_System_Driver.H b/src/Fl_System_Driver.H index e7230f74e..3dad3d754 100644 --- a/src/Fl_System_Driver.H +++ b/src/Fl_System_Driver.H @@ -110,6 +110,10 @@ public: virtual int rmdir(const char*) {return -1;} virtual int rename(const char* /*f*/, const char * /*n*/) {return -1;} + // Windows commandline argument conversion to UTF-8. + // Default implementation: no-op, overridden only on Windows + virtual int args_to_utf8(int argc, char ** &argv) { return argc; } + // the default implementation of these utf8... functions should be enough virtual unsigned utf8towc(const char* src, unsigned srclen, wchar_t* dst, unsigned dstlen); virtual unsigned utf8fromwc(char* dst, unsigned dstlen, const wchar_t* src, unsigned srclen); @@ -124,9 +128,9 @@ public: virtual int filename_list(const char * /*d*/, dirent ***, int (* /*sort*/)(struct dirent **, struct dirent **), char *errmsg=NULL, int errmsg_sz=0) { - (void)errmsg; (void)errmsg_sz; - return -1; - } + (void)errmsg; (void)errmsg_sz; + return -1; + } // the default implementation of filename_expand() may be enough virtual int filename_expand(char *to, int tolen, const char *from); // to implement diff --git a/src/drivers/WinAPI/Fl_WinAPI_System_Driver.H b/src/drivers/WinAPI/Fl_WinAPI_System_Driver.H index aae1017e2..016cad9fa 100644 --- a/src/drivers/WinAPI/Fl_WinAPI_System_Driver.H +++ b/src/drivers/WinAPI/Fl_WinAPI_System_Driver.H @@ -61,11 +61,15 @@ public: int mkdir(const char *fnam, int mode) FL_OVERRIDE; int rmdir(const char *fnam) FL_OVERRIDE; int rename(const char *fnam, const char *newnam) FL_OVERRIDE; + // Windows commandline argument conversion to UTF-8 + int args_to_utf8(int argc, char ** &argv) FL_OVERRIDE; + // Windows specific UTF-8 conversions unsigned utf8towc(const char *src, unsigned srclen, wchar_t* dst, unsigned dstlen) FL_OVERRIDE; unsigned utf8fromwc(char *dst, unsigned dstlen, const wchar_t* src, unsigned srclen) FL_OVERRIDE; int utf8locale() FL_OVERRIDE; unsigned utf8to_mb(const char *src, unsigned srclen, char *dst, unsigned dstlen) FL_OVERRIDE; unsigned utf8from_mb(char *dst, unsigned dstlen, const char *src, unsigned srclen) FL_OVERRIDE; + int clocale_vprintf(FILE *output, const char *format, va_list args) FL_OVERRIDE; int clocale_vsnprintf(char *output, size_t output_size, const char *format, va_list args) FL_OVERRIDE; int clocale_vsscanf(const char *input, const char *format, va_list args) FL_OVERRIDE; diff --git a/src/drivers/WinAPI/Fl_WinAPI_System_Driver.cxx b/src/drivers/WinAPI/Fl_WinAPI_System_Driver.cxx index faf89e979..d7b01465a 100644 --- a/src/drivers/WinAPI/Fl_WinAPI_System_Driver.cxx +++ b/src/drivers/WinAPI/Fl_WinAPI_System_Driver.cxx @@ -299,6 +299,40 @@ int Fl_WinAPI_System_Driver::rename(const char *fnam, const char *newnam) { return _wrename(wbuf, wbuf1); } +// See Fl::args_to_utf8() +int Fl_WinAPI_System_Driver::args_to_utf8(int argc, char ** &argv) { + int i; + char strbuf[2048]; // FIXME: allocate argv and strings dynamically + + // Convert the command line arguments to UTF-8 + LPWSTR *wideArgv = CommandLineToArgvW(GetCommandLineW(), &argc); + argv = (char **)malloc((argc + 1) * sizeof(char *)); + for (i = 0; i < argc; i++) { + int ret = WideCharToMultiByte(CP_UTF8, // CodePage + 0, // dwFlags + wideArgv[i], // lpWideCharStr + -1, // cchWideChar + strbuf, // lpMultiByteStr + sizeof(strbuf), // cbMultiByte + NULL, // lpDefaultChar + NULL); // lpUsedDefaultChar + + if (!ret) + strbuf[0] = '\0'; // return empty string + argv[i] = fl_strdup(strbuf); + } + argv[argc] = NULL; // required NULL pointer at end of list + + // Free the wide character string array + LocalFree(wideArgv); + + // Note: the allocated memory or argv[] will not be free'd by the system + // on exit. This does not constitute a memory leak. + + return argc; +} + + // Two Windows-specific functions fl_utf8_to_locale() and fl_locale_to_utf8() // from file fl_utf8.cxx are put here for API compatibility diff --git a/src/fl_call_main.c b/src/fl_call_main.c index 025e7f03c..de05b98d4 100644 --- a/src/fl_call_main.c +++ b/src/fl_call_main.c @@ -22,29 +22,40 @@ * "main()". This will allow you to build a Windows Application * without any special settings. * - * Because of problems with the Microsoft Visual C++ header files - * and/or compiler, you cannot have a WinMain function in a DLL. - * I don't know why. Thus, this nifty feature is only available - * if you link to the static library. + * You cannot have this WinMain() function in a DLL because it would have + * to call \c main() outside the DLL. Thus, this nifty feature is only + * available if you link to the static library. * - * Currently the debug version of this library will create a - * console window for your application so you can put printf() - * statements for debugging or informational purposes. Ultimately - * we want to update this to always use the parent's console, - * but at present we have not identified a function or API in - * Microsoft(r) Windows(r) that allows for it. + * However, it is possible to build this module separately so you can + * use it in progams that link to the shared library. + * + * Currently the debug version of this library will create a console window + * for your application so you can put printf() statements for debugging or + * informational purposes. Ultimately we want to update this to always use + * the parent's console, but at present we have not identified a function + * or API in Microsoft(r) Windows(r) that allows for it. */ /* - * This file is compiled only on Windows platforms (since FLTK 1.4.0). - * Therefore we don't need to test the _WIN32 macro anymore. - * The _MSC_VER macro is tested to compile it only for Visual Studio - * platforms because GNU platforms (MinGW, MSYS) don't need it. + * Notes for FLTK developers: + * + * 1) Since FLTK 1.4.0 this file is compiled only on Windows, hence we don't + * need to test the _WIN32 macro. + * 2) This file must not call any FLTK library functions because this would + * not work with /both/ the DLL /and/ the static library (linkage stuff). + * 3) Converting the commandline arguments to UTF-8 is therefore implemented + * here *and* in the library but this seems to be an acceptable compromise. + * 4) (Unless someone finds a better solution, of course. Albrecht) + * 5) The condition "!defined(FL_DLL)" prevents building this in the shared + * library, i.e. "WinMain()" will not be defined in the shared lib (DLL). + * 6) The condition "!defined (__GNUC__)" prevents compilation of this + * module with MinGW, MSYS, and Cygwin which don't use WinMain(). + * 7) It is unclear if there are other build systems on Windows that need a + * WinMain() entry point. Earlier comments and code seem to indicate that + * Borland C++ would require it. */ -#if !defined(FL_DLL) && !defined (__GNUC__) -#include -#include +#if !defined(FL_DLL) && !defined (__GNUC__) #include #include @@ -78,9 +89,13 @@ int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, freopen("conout$", "w", stderr); #endif /* _DEBUG */ - /* Convert the command line arguments to UTF-8 */ + /* Get the command line arguments as Windows Wide Character strings */ LPWSTR *wideArgv = CommandLineToArgvW(GetCommandLineW(), &argc); + + /* Allocate an array of 'argc + 1' string pointers */ argv = (char **)malloc((argc + 1) * sizeof(char *)); + + /* Convert the command line arguments to UTF-8 */ for (i = 0; i < argc; i++) { int ret = WideCharToMultiByte(CP_UTF8, /* CodePage */ 0, /* dwFlags */ @@ -90,7 +105,7 @@ int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, sizeof(strbuf), /* cbMultiByte */ NULL, /* lpDefaultChar */ NULL); /* lpUsedDefaultChar */ - argv[i] = fl_strdup(strbuf); + argv[i] = _strdup(strbuf); } argv[argc] = NULL; // required by C standard at end of list @@ -118,6 +133,8 @@ int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, } #else + /* STR# 2973: solves "empty translation unit" error */ typedef int dummy; + #endif /* !defined(FL_DLL) && !defined (__GNUC__) */ diff --git a/test/pixmap_browser.cxx b/test/pixmap_browser.cxx index dca13316e..9478540cd 100644 --- a/test/pixmap_browser.cxx +++ b/test/pixmap_browser.cxx @@ -1,7 +1,7 @@ // // A shared image test program for the Fast Light Tool Kit (FLTK). // -// Copyright 1998-2022 by Bill Spitzak and others. +// Copyright 1998-2023 by Bill Spitzak and others. // // This library is free software. Distribution and use rights are outlined in // the file "COPYING" which should have been included with this file. If this @@ -143,7 +143,8 @@ int main(int argc, char **argv) { setlocale(LC_ALL, ""); // enable multilanguage errors in file chooser fl_register_images(); - Fl::args(argc,argv,i,arg); + Fl::args_to_utf8(argc, argv); // enable multilanguage commandlines on Windows + Fl::args(argc, argv, i, arg); // parse commandline if (animate) Fl_GIF_Image::animate = true; // create animated shared .GIF images (e.g. file chooser)