"""EasyGUI_Qt: procedural gui based on PyQt EasyGUI_Qt is inspired by EasyGUI and contains a number of different basic graphical user interface components """ import os import sys import traceback import webbrowser from collections import OrderedDict if sys.version_info < (3,): import ConfigParser as configparser else: import configparser unicode = str try: from PyQt4 import QtGui, QtCore qt_widgets = QtGui _qt4 = True except ImportError: from PyQt5 import QtGui, QtCore from PyQt5 import QtWidgets as qt_widgets _qt4 = False try: from . import utils from . import language_selector from . import calendar_widget from . import multichoice from . import show_text_window from . import multifields except: import utils import language_selector import calendar_widget import multichoice import show_text_window import multifields __all__ = [ 'get_choice', 'get_list_of_choices', 'get_float', 'get_int', 'get_integer', 'get_string', 'get_many_strings', 'get_password', 'get_username_password', 'get_new_password', 'get_yes_or_no', 'get_continue_or_cancel', 'get_color_hex', 'get_color_rgb', 'get_date', 'get_directory_name', 'get_file_names', 'get_save_file_name', 'handle_exception', 'set_font_size', 'get_language', 'set_language', 'get_abort', 'show_message', 'show_file', 'show_text', 'show_code', 'show_html', 'find_help' ] QM_FILES = None class SimpleApp(qt_widgets.QApplication): """A simple extention of the basic QApplication with added methods useful for working with dialogs that are not class based. """ def __init__(self): super(SimpleApp, self).__init__([]) self.translator = QtCore.QTranslator() self.default_font = QtGui.QFont() if sys.version_info < (3,) : settings_path = ".easygui-qt2" else: settings_path = ".easygui-qt3" self.config_path = os.path.join(os.path.expanduser("~"), settings_path) try: self.load_config() self.setFont(self.default_font) except: pass self.save_config() def save_config(self): config = configparser.RawConfigParser() config.add_section('Configuration') config.set('Configuration', 'locale', self.config['locale']) config.set('Configuration', 'font-size', self.config['font-size']) with open(self.config_path, 'w') as configfile: config.write(configfile) def load_config(self): # Todo: make more robust config = configparser.RawConfigParser() self.config = {} try: config.read(self.config_path) self.config['locale'] = config.get('Configuration', 'locale') self.set_locale(self.config['locale'], save=False) self.config['font-size'] = config.getint('Configuration', 'font-size') self.set_font_size(self.config['font-size'], save=False) except: print("Problem encountered in load_config.") self.config = {'locale': 'default', 'font-size': 12} return def set_locale(self, locale, save=True): """Sets the language of the basic controls for PyQt from a locale - provided that the corresponding qm files are present in the PyQt distribution. """ global QM_FILES if QM_FILES is None: QM_FILES = utils.find_qm_files() if locale in QM_FILES: if self.translator.load("qt_" + locale, QM_FILES[locale]): self.installTranslator(self.translator) self.config['locale'] = locale else: print("language not available") elif locale == "default" and self.config['locale'] != 'default': self.removeTranslator(self.translator) self.translator = QtCore.QTranslator() self.config['locale'] = 'default' elif self.config['locale'] in QM_FILES: if self.translator.load("qt_" + self.config['locale'], QM_FILES[self.config['locale']]): self.installTranslator(self.translator) if save: self.save_config() def set_font_size(self, font_size, save=True): """Internal method to set font size. """ try: font_size = int(font_size) except: print("font_size should be an integer") return self.default_font.setPointSize(font_size) self.config['font-size'] = font_size self.setFont(self.default_font) if save: self.save_config() #========== Message Boxes ====================# def show_message(message="Message", title="Title"): """Simple message box. :param message: message string :param title: window title >>> import easygui_qt as easy >>> easy.show_message() .. image:: ../docs/images/show_message.png """ app = SimpleApp() box = qt_widgets.QMessageBox(None) box.setWindowTitle(title) box.setText(message) box.show() box.raise_() box.exec_() app.quit() def get_yes_or_no(message="Answer this question", title="Title"): """Simple yes or no question. :param question: Question (string) asked :param title: Window title (string) :return: ``True`` for "Yes", ``False`` for "No", and ``None`` for "Cancel". >>> import easygui_qt as easy >>> choice = easy.get_yes_or_no() .. image:: ../docs/images/yes_no_question.png """ app = SimpleApp() flags = qt_widgets.QMessageBox.Yes | qt_widgets.QMessageBox.No flags |= qt_widgets.QMessageBox.Cancel box = qt_widgets.QMessageBox() box.show() box.raise_() reply = box.question(None, title, message, flags) app.quit() if reply==qt_widgets.QMessageBox.Cancel: return None return reply == qt_widgets.QMessageBox.Yes def get_continue_or_cancel(message="Processed will be cancelled!", title="Title", continue_button_text="Continue", cancel_button_text="Cancel"): """Continue or cancel question, shown as a warning (i.e. more urgent than simple message) :param question: Question (string) asked :param title: Window title (string) :param continue_button_text: text to display on button :param cancel_button_text: text to display on button :return: True for "Continue", False for "Cancel" >>> import easygui_qt as easy >>> choice = easy.get_continue_or_cancel() .. image:: ../docs/images/get_continue_or_cancel.png """ app = SimpleApp() message_box = qt_widgets.QMessageBox(qt_widgets.QMessageBox.Warning, title, message, qt_widgets.QMessageBox.NoButton) message_box.addButton(continue_button_text, qt_widgets.QMessageBox.AcceptRole) message_box.addButton(cancel_button_text, qt_widgets.QMessageBox.RejectRole) message_box.show() message_box.raise_() reply = message_box.exec_() app.quit() return reply == qt_widgets.QMessageBox.AcceptRole #============= Color dialogs ================= def get_color_hex(): """Using a color dialog, returns a color in hexadecimal notation i.e. a string '#RRGGBB' or "None" if color dialog is dismissed. >>> import easygui_qt as easy >>> color = easy.get_color_hex() .. image:: ../docs/images/select_color.png """ app = SimpleApp() color = qt_widgets.QColorDialog.getColor(QtCore.Qt.white, None) app.quit() if color.isValid(): return color.name() def get_color_rgb(app=None): """Using a color dialog, returns a color in rgb notation i.e. a tuple (r, g, b) or "None" if color dialog is dismissed. >>> import easygui_qt as easy >>> easy.set_language('fr') >>> color = easy.get_color_rgb() .. image:: ../docs/images/select_color_fr.png """ app = SimpleApp() color = qt_widgets.QColorDialog.getColor(QtCore.Qt.white, None) app.quit() if color.isValid(): return (color.red(), color.green(), color.blue()) #================ Date =================== def get_date(title="Select Date"): """Calendar widget :param title: window title :return: the selected date as a ``datetime.date`` instance >>> import easygui_qt as easy >>> date = easy.get_date() .. image:: ../docs/images/get_date.png """ app = SimpleApp() cal = calendar_widget.CalendarWidget(title=title) app.exec_() date = cal.date.toPyDate() return date #================ language/locale related def get_language(title="Select language", name="Language codes", instruction=None): """Dialog to choose language based on some locale code for files found on default path. :param title: Window title :param name: Heading for valid values of locale appearing in checkboxes :param instruction: Like the name says; when set to None, a default string is used which includes the current language used. The first time an EasyGUI_Qt widget is created in a program, the PyQt language files found in the standard location of the user's computer are scanned and recorded; these provide some translations of standard GUI components (like name of buttons). Note that "en" is not found as a locale (at least, not on the author's computer) but using "default" reverts the choice to the original (English here). >>> import easygui_qt as easy >>> easy.get_language() .. image:: ../docs/images/get_language.png """ app = SimpleApp() if instruction is None: instruction = ('Current language code is "{}".'.format( app.config['locale'])) selector = language_selector.LanguageSelector(app, title=title, name=name, instruction=instruction) selector.exec_() app.quit() def set_language(locale): """Sets the locale, if available :param locale: standard code for locale (e.g. 'fr', 'en_CA') Does not create a GUI widget, but affect the appearance of widgets created afterwards >>> import easygui_qt as easy >>> easy.set_locale('es') >>> # after setting the locale >>> easy.get_yes_or_no() .. image:: ../docs/images/after_set_locale.png """ app = SimpleApp() app.set_locale(locale) app.quit() return locale #=========== InputDialogs ======================== def get_common_input_flags(): '''avoiding copying same flags in all functions''' flags = QtCore.Qt.WindowSystemMenuHint | QtCore.Qt.WindowTitleHint flags |= QtCore.Qt.WindowStaysOnTopHint return flags class VisibleInputDialog(qt_widgets.QInputDialog): '''A simple InputDialog class that attempts to make itself automatically on all platforms ''' def __init__(self): super(VisibleInputDialog, self).__init__() self.show() self.raise_() def get_int(message="Choose a number", title="Title", default_value=1, min_=0, max_=100, step=1): """Simple dialog to ask a user to select an integer within a certain range. **Note**: **get_int()** and **get_integer()** are identical. :param message: Message displayed to the user, inviting a response :param title: Window title :param default_value: Default value for integer appearing in the text box; set to the closest of ``min_`` or ``max_`` if outside of allowed range. :param min_: Minimum integer value allowed :param max_: Maximum integer value allowed :param step: Indicate the change in integer value when clicking on arrows on the right hand side :return: an integer, or ``None`` if "cancel" is clicked or window is closed. >>> import easygui_qt as easy >>> number = easy.get_int() .. image:: ../docs/images/get_int.png If ``default_value`` is larger than ``max_``, it is set to ``max_``; if it is smaller than ``min_``, it is set to ``min_``. >>> number = easy.get_integer("Enter a number", default_value=125) .. image:: ../docs/images/get_int2.png """ # converting values to int for launcher demo set_font_size which # first queries the user for a value; the initial values are passed # as strings by the subprocess module and need to be converted here default_value = int(default_value) min_ = int(min_) max_ = int(max_) app = SimpleApp() dialog = VisibleInputDialog() flags = get_common_input_flags() if _qt4: number, ok = dialog.getInteger(None, title, message, default_value, min_, max_, step, flags) else: number, ok = dialog.getInt(None, title, message, default_value, min_, max_, step, flags) dialog.destroy() app.quit() if ok: return number get_integer = get_int def get_float(message="Choose a number", title="Title", default_value=0.0, min_=-10000, max_=10000, decimals=3): """Simple dialog to ask a user to select a floating point number within a certain range and a maximum precision. :param message: Message displayed to the user, inviting a response :param title: Window title :param default_value: Default value for value appearing in the text box; set to the closest of ``min_`` or ``max_`` if outside of allowed range. :param min_: Minimum value allowed :param max_: Maximum value allowed :param decimals: Indicate the maximum decimal precision allowed :return: a floating-point number, or ``None`` if "cancel" is clicked or window is closed. >>> import easygui_qt as easy >>> number = easy.get_float() .. image:: ../docs/images/get_float.png **Note:** depending on the locale of the operating system where this is used, instead of a period being used for indicating the decimals, a comma may appear instead; this is the case for the French version of Windows for example. Therefore, entry of floating point values in this situation will require the use of a comma instead of a period. However, the internal representation will still be the same, and the number passed to Python will be using the familar notation. """ app = SimpleApp() dialog = VisibleInputDialog() flags = get_common_input_flags() number, ok = dialog.getDouble(None, title, message, default_value, min_, max_, decimals, flags) app.quit() if ok: return number def get_string(message="Enter your response", title="Title", default_response=""): """Simple text input box. Used to query the user and get a string back. :param message: Message displayed to the user, inviting a response :param title: Window title :param default_response: default response appearing in the text box :return: a string, or ``None`` if "cancel" is clicked or window is closed. >>> import easygui_qt as easy >>> reply = easy.get_string() .. image:: ../docs/images/get_string.png >>> reply = easy.get_string("new message", default_response="ready") .. image:: ../docs/images/get_string2.png """ app = SimpleApp() dialog = VisibleInputDialog() flags = get_common_input_flags() text, ok = dialog.getText(None, title, message, qt_widgets.QLineEdit.Normal, default_response, flags) app.quit() if ok: if sys.version_info < (3,): return unicode(text) return text def get_password(message="Enter your password", title="Title"): """Simple password input box. Used to query the user and get a string back. :param message: Message displayed to the user, inviting a response :param title: Window title :return: a string, or ``None`` if "cancel" is clicked or window is closed. >>> import easygui_qt as easy >>> password = easy.get_password() .. image:: ../docs/images/get_password.png """ app = SimpleApp() dialog = VisibleInputDialog() flags = get_common_input_flags() text, ok = dialog.getText(None, title, message, qt_widgets.QLineEdit.Password, '', flags) app.quit() if ok: if sys.version_info < (3,): return unicode(text) return text def get_choice(message="Select one item", title="Title", choices=None): """Simple dialog to ask a user to select an item within a drop-down list :param message: Message displayed to the user, inviting a response :param title: Window title :param choices: iterable (list or tuple) containing the names of the items that can be selected. :returns: a string, or ``None`` if "cancel" is clicked or window is closed. >>> import easygui_qt as easy >>> choices = ["CPython", "Pypy", "Jython", "IronPython"] >>> reply = easy.get_choice("What is the best Python implementation", ... choices=choices) .. image:: ../docs/images/get_choice.png """ if choices is None: choices = ["Item 1", "Item 2", "Item 3", "Item 4", "Item 5"] app = SimpleApp() dialog = VisibleInputDialog() flags = get_common_input_flags() choice, ok = dialog.getItem(None, title, message, choices, 0, False, flags) app.quit() if ok: if sys.version_info < (3,): return unicode(choice) return choice def get_username_password(title="Title", labels=None): """User name and password input box. :param title: Window title :param labels: an iterable containing the labels for "user name" and "password"; if the value not specified, the default values will be used. :return: An ordered dict containing the fields item as keys, and the input from the user (empty string by default) as value Note: this function is a special case of ``get_many_strings`` where the required masks are provided automatically.. >>> import easygui_qt as easy >>> reply = easy.get_username_password() >>> reply OrderedDict([('User name', 'aroberge'), ('Password', 'not a good password')]) .. image:: ../docs/images/get_username_password.png """ if labels is None: labels = ["User name", "Password"] if len(labels) != 2: _title = "Error found" message = "labels should have 2 elements; {} were found".format(len(labels)) get_abort(title=_title, message=message) masks = [False, True] return get_many_strings(title=title, labels=labels, masks=masks) def get_new_password(title="Title", labels=None): """Change password input box. :param title: Window title :param labels: an iterable containing the labels for "Old password" and "New password" and "Confirm new password". All three labels must be different strings as they are used as keys in a dict - however, they could differ only by a space. :return: An ordered dict containing the fields item as keys, and the input from the user as values. Note: this function is a special case of ``get_many_strings`` where the required masks are provided automatically.. >>> import easygui_qt as easy >>> reply = easy.get_new_password() .. image:: ../docs/images/get_new_password.png """ if not labels: # empty list acceptable for test labels = ["Old password:", "New password:", "Confirm new password:"] if len(labels) != 3: _title = "Error found" message = "labels should have 3 elements; {} were found".format(len(labels)) get_abort(title=_title, message=message) masks = [True, True, True] class Parent: pass parent = Parent() app = SimpleApp() dialog = multifields.MultipleFieldsDialog(labels=labels, masks=masks, parent=parent, title=title) dialog.exec_() app.quit() return parent.o_dict def get_many_strings(title="Title", labels=None, masks=None): """Multiple strings input :param title: Window title :param labels: an iterable containing the labels for to use for the entries :param masks: optional parameter. :return: An ordered dict containing the labels as keys, and the input from the user (empty string by default) as value The parameter ``masks`` if set must be an iterable of the same length as ``choices`` and contain either True or False as entries indicating if the entry of the text is masked or not. For example, one could ask for a username and password using get_many_strings as follows [note that get_username_password exists and automatically takes care of specifying the masks and is a better choice for this use case.] >>> import easygui_qt as easy >>> labels = ["User name", 'Password'] >>> masks = [False, True] >>> reply = easy.get_many_strings(labels=labels, masks=masks) >>> reply OrderedDict([('User name', 'aroberge'), ('Password', 'not a good password')]) .. image:: ../docs/images/get_many_strings.png """ class Parent: pass parent = Parent() app = SimpleApp() dialog = multifields.MultipleFieldsDialog(labels=labels, masks=masks, parent=parent, title=title) dialog.exec_() app.quit() class IndexedOrderedDict(OrderedDict): def __getitem__(self,key): if isinstance(key,int): i=0 for v in self.values(): if i==key: return v i=i+1 return super().__getitem__(key) return IndexedOrderedDict(parent.o_dict) def get_list_of_choices(title="Title", choices=None): """Show a list of possible choices to be selected. :param title: Window title :param choices: iterable (list, tuple, ...) containing the choices as strings :returns: a list of selected items, otherwise an empty list. >>> import easygui_qt as easy >>> choices = easy.get_list_of_choices() .. image:: ../docs/images/get_list_of_choices.png """ app = SimpleApp() dialog = multichoice.MultipleChoicesDialog(title=title, choices=choices) dialog.exec_() app.quit() if sys.version_info < (3,): return [unicode(item) for item in dialog.selection] return dialog.selection #========== Files & directory dialogs def get_directory_name(title="Get directory"): '''Gets the name (full path) of an existing directory :param title: Window title :return: the name of a directory or an empty string if cancelled. >>> import easygui_qt as easy >>> easy.get_directory_name() .. image:: ../docs/images/get_directory_name.png By default, this dialog initially displays the content of the current working directory. ''' app = SimpleApp() options = qt_widgets.QFileDialog.Options() # Without the following option (i.e. using native dialogs), # calling this function twice in a row made Python crash. options |= qt_widgets.QFileDialog.DontUseNativeDialog options |= qt_widgets.QFileDialog.DontResolveSymlinks options |= qt_widgets.QFileDialog.ShowDirsOnly directory = qt_widgets.QFileDialog.getExistingDirectory(None, title, os.getcwd(), options) app.quit() if sys.version_info < (3,): return unicode(directory) return directory def get_file_names(title="Get existing file names"): '''Gets the names (full path) of existing files :param title: Window title :return: the list of names (paths) of files selected. (It can be an empty list.) >>> import easygui_qt as easy >>> easy.get_file_names() .. image:: ../docs/images/get_file_names.png By default, this dialog initially displays the content of the current working directory. ''' app = SimpleApp() if sys.version_info < (3,): files = qt_widgets.QFileDialog.getOpenFileNames(None, title, os.getcwd(), "All Files (*.*)") files = [unicode(item) for item in files] else: options = qt_widgets.QFileDialog.Options() options |= qt_widgets.QFileDialog.DontUseNativeDialog files = qt_widgets.QFileDialog.getOpenFileNames(None, title, os.getcwd(), "All Files (*.*)", options) app.quit() return files def get_save_file_name(title="File name to save"): '''Gets the name (full path) of of a file to be saved. :param title: Window title :return: the name (path) of file selected The user is warned if the file already exists and can choose to cancel. However, this dialog actually does NOT save any file: it only return a string containing the full path of the chosen file. >>> import easygui_qt as easy >>> easy.get_save_file_name() .. image:: ../docs/images/get_save_file_name.png By default, this dialog initially displays the content of the current working directory. ''' app = SimpleApp() if sys.version_info < (3,): file_name = qt_widgets.QFileDialog.getSaveFileName(None, title, os.getcwd(), "All Files (*.*)") app.quit() return unicode(file_name) options = qt_widgets.QFileDialog.Options() options |= qt_widgets.QFileDialog.DontUseNativeDialog # see get_directory_name file_name = qt_widgets.QFileDialog.getSaveFileName(None, title, os.getcwd(), "All Files (*.*)", options) app.quit() return file_name #========= Font related def set_font_size(font_size): """Simple method to set font size. :param font_size: integer value Does not create a GUI widget; but affects the appearance of future GUI widgets. >>> import easygui_qt as easy >>> easy.set_font_size(20) >>> easy.show_message() .. image:: ../docs/images/set_font_size.png """ app = SimpleApp() app.set_font_size(font_size) app.quit() print(font_size) # info for launcher def show_file(file_name=None, title="Title", file_type="text"): '''Displays a file in a window. While it looks as though the file can be edited, the only changes that happened are in the window and nothing can be saved. :param title: the window title :param file_name: the file name, (path) relative to the calling program :param file_type: possible values: ``text``, ``code``, ``html``, ``python``. By default, file_type is assumed to be ``text``; if set to ``code``, the content is displayed with a monospace font and, if set to ``python``, some code highlighting is done. If the file_type is ``html``, it is processed assuming it follows html syntax. **Note**: a better Python code hightlighter would be most welcome! >>> import easygui_qt as easy >>> easy.show_file() .. image:: ../docs/images/show_file.png ''' app = SimpleApp() editor = show_text_window.TextWindow(file_name=file_name, title=title, text_type=file_type) editor.show() app.exec_() def show_text(title="Title", text=""): '''Displays some text in a window. :param title: the window title :param code: a string to display in the window. >>> import easygui_qt as easy >>> easy.show_code() .. image:: ../docs/images/show_text.png ''' app = SimpleApp() editor = show_text_window.TextWindow(title=title, text_type='text', text=text) editor.resize(720, 450) editor.show() app.exec_() def show_code(title="Title", text=""): '''Displays some text in a window, in a monospace font. :param title: the window title :param code: a string to display in the window. >>> import easygui_qt as easy >>> easy.show_code() .. image:: ../docs/images/show_code.png ''' app = SimpleApp() editor = show_text_window.TextWindow(title=title, text_type='code', text=text) editor.resize(720, 450) editor.show() app.exec_() def show_html(title="Title", text=""): '''Displays some html text in a window. :param title: the window title :param code: a string to display in the window. >>> import easygui_qt as easy >>> easy.show_html() .. image:: ../docs/images/show_html.png ''' app = SimpleApp() editor = show_text_window.TextWindow(title=title, text_type='html', text=text) editor.resize(720, 450) editor.show() app.exec_() def get_abort(message="Major problem - or at least we think there is one...", title="Major problem encountered!"): '''Displays a message about a problem. If the user clicks on "abort", sys.exit() is called and the program ends. If the user clicks on "ignore", the program resumes its execution. :param title: the window title :param message: the message to display >>> import easygui_qt as easy >>> easy.get_abort() .. image:: ../docs/images/get_abort.png ''' app = SimpleApp() reply = qt_widgets.QMessageBox.critical(None, title, message, qt_widgets.QMessageBox.Abort | qt_widgets.QMessageBox.Ignore) if reply == qt_widgets.QMessageBox.Abort: sys.exit() else: pass app.quit() def handle_exception(title="Exception raised!"): '''Displays a traceback in a window if an exception is raised. If the user clicks on "abort", sys.exit() is called and the program ends. If the user clicks on "ignore", the program resumes its execution. :param title: the window title .. image:: ../docs/images/handle_exception.png ''' try: message = "\n".join(traceback.format_exception(sys.exc_info()[0], sys.exc_info()[1] , sys.exc_info()[2])) except AttributeError: return "No exception was raised" get_abort(title=title, message=message) def find_help(): '''Opens a web browser, pointing at the documention about EasyGUI_Qt available on the web. ''' webbrowser.open('http://easygui-qt.readthedocs.org/en/latest/api.html') if __name__ == '__main__': try: from demos import guessing_game guessing_game.guessing_game() except ImportError: print("Could not find demo.")