Source code for easygui_qt.easygui_qt

"""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 ====================#
[docs]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()
[docs]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
[docs]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 =================
[docs]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()
[docs]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 ===================
[docs]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
[docs]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()
[docs]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_()
[docs]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
[docs]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
[docs]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
[docs]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
[docs]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
[docs]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)
[docs]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
[docs]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)
[docs]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
[docs]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
[docs]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
[docs]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
[docs]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
[docs]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_()
[docs]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_()
[docs]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_()
[docs]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_()
[docs]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()
[docs]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)
[docs]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.")