{"id":1174,"date":"2019-12-13T11:51:22","date_gmt":"2019-12-13T11:51:22","guid":{"rendered":"https:\/\/www.pyxll.com\/blog\/?p=983"},"modified":"2020-06-05T09:51:05","modified_gmt":"2020-06-05T09:51:05","slug":"asking-the-user-for-input","status":"publish","type":"post","link":"https:\/\/www.pyxll.com\/blog\/asking-the-user-for-input\/","title":{"rendered":"Asking the User for Input with PyQt5"},"content":{"rendered":"\n<h4 class=\"has-vivid-cyan-blue-color has-text-color wp-block-heading\"><strong>Part 2 of our guide to using PyQt5 in Excel<\/strong><\/h4>\n\n\n\n<p>You often need to get small amounts of input, or offer a choice among a number of alternatives. Qt, like all GUI systems, offers you a choice of graphical elements collectively known as <em>widgets<\/em>. In this post we introduce you to the high-level QDialog widget, and some of the simpler input widgets.<\/p>\n\n\n\n<p>Building GUIs for Excel need not be painful. It is quite simple to hook up a few widgets inside a QDialog to get the input you need.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Meet the&nbsp;QDialog<\/h2>\n\n\n\n<p>If you&#8217;ve already read <em><a href=\"https:\/\/www.pyxll.com\/blog\/extending-the-excel-user-interface\/\" target=\"_blank\" rel=\"noreferrer noopener\" aria-label=\" (opens in a new tab)\">Extending the Excel User Interface<\/a><\/em> you are already familiar with a subclass of QDialog. The QMessageBox used in the last <a href=\"https:\/\/www.pyxll.com\/blog\/extending-the-excel-user-interface\/\" target=\"_blank\" rel=\"noreferrer noopener\" aria-label=\" (opens in a new tab)\">blog entry<\/a> is one of QDialog&#8217;s simpler derivative classes. There&#8217;s a fuller introduction to the QDialog <a href=\"https:\/\/www.learnpyqt.com\/courses\/start\/dialogs\/\">in this <\/a><a href=\"https:\/\/www.learnpyqt.com\/courses\/start\/dialogs\/\" target=\"_blank\" rel=\"noreferrer noopener\" aria-label=\"t (opens in a new tab)\">t<\/a><a href=\"https:\/\/www.learnpyqt.com\/courses\/start\/dialogs\/\">utorial<\/a>.<\/p>\n\n\n\n<p>By extending (inheriting from) QDialog it&#8217;s simple to create your own dialogues with various input widgets and standard buttons. The widgets go into a <em>layout<\/em> object. Once complete you set that as the QDialog&#8217;s layout and your dialog is ready for action.<\/p>\n\n\n\n<p>The simplest way of establishing a layout uses the QHBoxLayout and the QVBoxLayout objects to position a sequence of widgets together either next to each other or above one another respectively. Since the layout boxes are themselves widgets they can be placed inside other groupings.<\/p>\n\n\n\n<p>To avoid problems of parallelization it is by far simplest to use <em>modal<\/em> dialogues, where all other program activity is suspended until the dialogue is dismissed. That way you know that Excel won&#8217;t be changing things while the dialogue is being processed. QDialogs are modal by default.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Desired Layout<\/h3>\n\n\n\n<p>In the case of the optimization, we need to ask for a range of input cells and an output cell. A straightforward way to do this is to have two rows (QHLayoutBoxes) each containing a label (a QLabel widget)  and an input box (a QTextEdit widget), inside a QVLayoutBox. You can conceptualise it something like this:<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img decoding=\"async\" width=\"1024\" height=\"714\" data-attachment-id=\"1388\" data-permalink=\"https:\/\/www.pyxll.com\/blog\/asking-the-user-for-input\/image-5-2\/\" data-orig-file=\"https:\/\/www.pyxll.com\/blog\/wp-content\/uploads\/2020\/06\/image-5.png\" data-orig-size=\"1177,821\" data-comments-opened=\"0\" data-image-meta=\"{&quot;aperture&quot;:&quot;0&quot;,&quot;credit&quot;:&quot;&quot;,&quot;camera&quot;:&quot;&quot;,&quot;caption&quot;:&quot;&quot;,&quot;created_timestamp&quot;:&quot;0&quot;,&quot;copyright&quot;:&quot;&quot;,&quot;focal_length&quot;:&quot;0&quot;,&quot;iso&quot;:&quot;0&quot;,&quot;shutter_speed&quot;:&quot;0&quot;,&quot;title&quot;:&quot;&quot;,&quot;orientation&quot;:&quot;0&quot;}\" data-image-title=\"image-5\" data-image-description=\"\" data-image-caption=\"\" data-large-file=\"https:\/\/www.pyxll.com\/blog\/wp-content\/uploads\/2020\/06\/image-5-1024x714.png\" src=\"https:\/\/www.pyxll.com\/blog\/wp-content\/uploads\/2020\/06\/image-5-1024x714.png\" alt=\"\" class=\"wp-image-1388\" srcset=\"https:\/\/www.pyxll.com\/blog\/wp-content\/uploads\/2020\/06\/image-5-1024x714.png 1024w, https:\/\/www.pyxll.com\/blog\/wp-content\/uploads\/2020\/06\/image-5-300x209.png 300w, https:\/\/www.pyxll.com\/blog\/wp-content\/uploads\/2020\/06\/image-5-768x536.png 768w, https:\/\/www.pyxll.com\/blog\/wp-content\/uploads\/2020\/06\/image-5.png 1177w\" sizes=\"(max-width: 1024px) 100vw, 1024px\" \/><figcaption>The required layout is a QVBoxLayout (shown in purple) containing two<br>QHBoxLayouts (green), each of which contains a QLabel (red) and a QLineEdit (black).<\/figcaption><\/figure>\n\n\n\n<p>Here is a first attempt at a class definition to implement this interface in a standalone program. I&#8217;ve used my standard strategy of creating the widgets first and then adding the widgets to the layouts. I&#8217;ve missed out the imports and the get_qt_app function. The whole program is qt_ex2.py in the repository.<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: python; title: ; notranslate\" title=\"\">\nclass OpDialog(QDialog):\n    &quot;A Dialog to set input and output ranges for an optimization.&quot;\n\n    def __init__(self, *args, **kwargs):\n        &quot;Create a new dialogue instance.&quot;\n        super().__init__(*args, **kwargs)\n        self.setWindowTitle(&quot;Optimization Inputs and Output&quot;)\n        self.gui_init()\n\n    def gui_init(self):\n        &quot;Create and establish the widget layout.&quot;\n        self.in_range = QLineEdit()\n        self.out_cell = QLineEdit()\n\n        row_1 = QHBoxLayout()\n        row_1.addWidget(QLabel(&quot;Input range:&quot;))\n        row_1.addWidget(self.in_range)\n\n        row_2 = QHBoxLayout()\n        row_2.addWidget(QLabel(&quot;Output Cell:&quot;))\n        row_2.addWidget(self.out_cell)\n\n        layout = QVBoxLayout()\n        layout.addLayout(row_1)\n        layout.addLayout(row_2)\n        self.setLayout(layout)\n<\/pre><\/div>\n\n\n<p>This displays a window that looks like this:<\/p>\n\n\n\n<figure class=\"wp-block-image size-large no-shadow\"><img decoding=\"async\" width=\"428\" height=\"216\" data-attachment-id=\"1330\" data-permalink=\"https:\/\/www.pyxll.com\/blog\/asking-the-user-for-input\/image-7-2\/\" data-orig-file=\"https:\/\/www.pyxll.com\/blog\/wp-content\/uploads\/2020\/01\/image-7.png\" data-orig-size=\"428,216\" data-comments-opened=\"0\" data-image-meta=\"{&quot;aperture&quot;:&quot;0&quot;,&quot;credit&quot;:&quot;&quot;,&quot;camera&quot;:&quot;&quot;,&quot;caption&quot;:&quot;&quot;,&quot;created_timestamp&quot;:&quot;0&quot;,&quot;copyright&quot;:&quot;&quot;,&quot;focal_length&quot;:&quot;0&quot;,&quot;iso&quot;:&quot;0&quot;,&quot;shutter_speed&quot;:&quot;0&quot;,&quot;title&quot;:&quot;&quot;,&quot;orientation&quot;:&quot;0&quot;}\" data-image-title=\"image-7\" data-image-description=\"\" data-image-caption=\"\" data-large-file=\"https:\/\/www.pyxll.com\/blog\/wp-content\/uploads\/2020\/01\/image-7.png\" src=\"https:\/\/www.pyxll.com\/blog\/wp-content\/uploads\/2020\/01\/image-7.png\" alt=\"\" class=\"wp-image-1330\" srcset=\"https:\/\/www.pyxll.com\/blog\/wp-content\/uploads\/2020\/01\/image-7.png 428w, https:\/\/www.pyxll.com\/blog\/wp-content\/uploads\/2020\/01\/image-7-300x151.png 300w\" sizes=\"(max-width: 428px) 100vw, 428px\" \/><\/figure>\n\n\n\n<h3 class=\"wp-block-heading\">Stretch goals<\/h3>\n\n\n\n<p>Note that the QLineEdit widgets are displayed at different widths. This is because layouts by default try to fill their space with the widgets. You can add &#8220;stretch&#8221; to a layout, and that will take up the slack and allow visible widgets to adopt this natural length. The qt_ex3.py program changes the QHBoxLayout creation code by inserting calls to their addStretch method.<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: python; title: ; notranslate\" title=\"\">\n        row_1 = QHBoxLayout()\n        row_1.addWidget(QLabel(&quot;Input range:&quot;))\n        row_1.addStretch()\n        row_1.addWidget(self.in_range)\n\n        row_2 = QHBoxLayout()\n        row_2.addWidget(QLabel(&quot;Output Cell:&quot;))\n        row_2.addStretch()\n        row_2.addWidget(self.out_cell)\n<\/pre><\/div>\n\n\n<p>These small differences can make the difference in people&#8217;s perception of an interface, so it&#8217;s worth paying attention to them if you aren&#8217;t going to be the only user. The updated dialogue box looks like this.<\/p>\n\n\n\n<figure class=\"wp-block-image size-large no-shadow\"><img decoding=\"async\" width=\"428\" height=\"214\" data-attachment-id=\"1329\" data-permalink=\"https:\/\/www.pyxll.com\/blog\/asking-the-user-for-input\/image-8-2\/\" data-orig-file=\"https:\/\/www.pyxll.com\/blog\/wp-content\/uploads\/2020\/01\/image-8.png\" data-orig-size=\"428,214\" data-comments-opened=\"0\" data-image-meta=\"{&quot;aperture&quot;:&quot;0&quot;,&quot;credit&quot;:&quot;&quot;,&quot;camera&quot;:&quot;&quot;,&quot;caption&quot;:&quot;&quot;,&quot;created_timestamp&quot;:&quot;0&quot;,&quot;copyright&quot;:&quot;&quot;,&quot;focal_length&quot;:&quot;0&quot;,&quot;iso&quot;:&quot;0&quot;,&quot;shutter_speed&quot;:&quot;0&quot;,&quot;title&quot;:&quot;&quot;,&quot;orientation&quot;:&quot;0&quot;}\" data-image-title=\"image-8\" data-image-description=\"\" data-image-caption=\"\" data-large-file=\"https:\/\/www.pyxll.com\/blog\/wp-content\/uploads\/2020\/01\/image-8.png\" src=\"https:\/\/www.pyxll.com\/blog\/wp-content\/uploads\/2020\/01\/image-8.png\" alt=\"\" class=\"wp-image-1329\" srcset=\"https:\/\/www.pyxll.com\/blog\/wp-content\/uploads\/2020\/01\/image-8.png 428w, https:\/\/www.pyxll.com\/blog\/wp-content\/uploads\/2020\/01\/image-8-300x150.png 300w\" sizes=\"(max-width: 428px) 100vw, 428px\" \/><\/figure>\n\n\n\n<h3 class=\"wp-block-heading\">Buttoning it down<\/h3>\n\n\n\n<p>The dialogue layout looks good, but the user has no way to indicate that they have finished entry! Another nice feature of the QDialog is that you can add most of the standard buttons in a neat QDialogButtonBox widget, whose argument is a set of flags selecting the required buttons. Using this widget also helps you to conform with UI guidelines for the Windows platform (and others, if you use them).<\/p>\n\n\n\n<p>The QDialog has a number of standard methods, including <em>accept<\/em> and <em>reject<\/em>, used to indicate whether the QDialog was dismissed with (<em>e.g.<\/em>) the OK or the Cancel button. If the former then the QDialog&#8217;s <em>exec_<\/em> method returns True, otherwise False. These need to be wired to the signals raised by clicking the buttons.<\/p>\n\n\n\n<p>The code to add the buttons and connect the to the appropriate methods is shown below (new lines are highlighted, and the complete program is included as op_dialog.py in the repository).<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: python; highlight: [1,2,3,4,5,6,7,8,13]; title: ; notranslate\" title=\"\">\n        row_3 = QHBoxLayout()\n        self.buttonBox = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)\n        self.buttonBox.accepted.connect(self.accept)\n        self.buttonBox.rejected.connect(self.reject)\n        row_3.addWidget(self.buttonBox)\n\n        self.layout = QVBoxLayout()\n        self.layout.addWidget(self.buttonBox)\n\n        layout = QVBoxLayout()\n        layout.addLayout(row_1)\n        layout.addLayout(row_2)\n        layout.addLayout(row_3)\n        self.setLayout(layout)\n<\/pre><\/div>\n\n\n<p>You can determine whether the user pressed&#8221;Cancel&#8221; or &#8220;OK&#8221; by examining the result of the QDialog&#8217;s exec_ method, and read the values from the QLineEdit items once exec_ has returned. The updated main program&#8217;s code reflects this.<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: python; title: ; notranslate\" title=\"\">\nif __name__ == &quot;__main__&quot;:\n    app = get_qt_app()\n    msgBox = OpDialog()\n    result = msgBox.exec_()\n    print(&quot;Input range:&quot;, msgBox.in_range.text())\n    print(&quot;Output cell:&quot;, msgBox.out_cell.text())\n    print(&quot;You clicked&quot;, &quot;OK&quot; if result else &quot;Cancel&quot;)\n<\/pre><\/div>\n\n\n<h2 class=\"wp-block-heading\">Updating the optimizer<\/h2>\n\n\n\n<p>At this point you have built the dialogue you need as a class. The main program does not run when the module is imported. Other modules can therefore import and use the dialogue instead of copying and pasting. Sharing is so much easier in Python than in VBA &#8211; now all your workbooks can use this dialogue!<\/p>\n\n\n\n<p>Because the inputs are expected to be cell and range references, I&#8217;ve added a small verifier function to validate the inputs. A simple strategy tries to return an Excel range. Any exceptions must be caused by &#8220;bad&#8221; user input. <\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: python; title: ; notranslate\" title=\"\">\ndef get_range(s):\n    try:\n        return xl.Range(s)\n    except Exception as e:\n        raise ValueError(&quot;Range specification not acceptable&quot;)\n<\/pre><\/div>\n\n\n<p>The logic of the optimizer function is changed so that it obtains the input and output ranges from the dialogue box rather than using fixed references. Otherwise the code remains essentially the same.<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: python; title: ; notranslate\" title=\"\">\n@xl_menu(&quot;Optimize5&quot;)\ndef optimize5():\n    &quot;&quot;&quot;\n    Trigger optimization of a spreadsheet model that\n    takes the named range &quot;Inputs&quot; as inputs and\n    produces output in the named range &quot;Output&quot;.\n    &quot;&quot;&quot;\n    xl = xl_app()\n    qt_app = get_qt_app()  # pragma noqc\n    # Get the initial values of the input cells\n    msgBox = OpDialog()\n    result = msgBox.exec_()\n    if not result:  # user cancelled\n        return\n    in_range = get_range(msgBox.in_range.text())\n    out_cell = get_range(msgBox.out_cell.text())\n    in_values = list(in_range.Value)\n    X = np.array(&#x5B;x&#x5B;0] for x in in_values])\n\n    orig_calc_mode = xl.Calculation\n    try:\n        # switch Excel to manual calculation\n        # and disable screen updating\n        xl.Calculation = constants.xlManual\n        xl.ScreenUpdating = False\n\n        # run the minimization routine\n        xl_obj_func = partial(obj_func, in_range, out_cell, xl)\n        result = minimize(xl_obj_func, X, method=&quot;nelder-mead&quot;)\n        xl.ScreenUpdating = True\n        mbox = QMessageBox()\n        mbox.setIcon(QMessageBox.Information)\n        mbox.setText(&quot;Optimization results shown below.&quot; &quot;\\nMake changes permanent?&quot;)\n        mbox.setWindowTitle(&quot;Optimization Complete&quot;)\n        mbox.setInformativeText(\n            &quot;\\n&quot;.join(\n                &#x5B;\n                    &quot;Successful:       %s&quot; % result.success,\n                    result.message,\n                    &quot;After %d iterations&quot; % result.nit,\n                ]\n            )\n        )\n        mbox.setStandardButtons(QMessageBox.Ok | QMessageBox.Cancel)\n        yes_no = mbox.exec_()\n        if yes_no != QMessageBox.Ok:\n            in_range.Value = in_values\n        else:\n            in_range.Value = &#x5B;(float(x),) for x in result.x]\n\n    finally:\n        # restore the original calculation\n        # and screen updating mode\n        xl.ScreenUpdating = True\n        xl.Calculation = orig_calc_mode\n<\/pre><\/div>\n\n\n<p>Now when you select &#8220;Optimize5&#8221; from the PyXLL menu the dialogue box asks you for the input range and the output cell for the model. These are also passed in to the objective function by extending the functools.partial argument list.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Part 2 of our guide to create user interfaces in Excel using PyQt5. This tutorial covers creating dialogs and collecting input from the user.<\/p>\n","protected":false},"author":803,"featured_media":1175,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"jetpack_post_was_ever_published":false,"_jetpack_newsletter_access":"","_jetpack_dont_email_post_to_subs":false,"_jetpack_newsletter_tier_id":0,"_jetpack_memberships_contains_paywalled_content":false,"_jetpack_memberships_contains_paid_content":false,"footnotes":""},"categories":[12,2,504],"tags":[6,503,3,8],"class_list":["post-1174","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-python","category-pyxll","category-user-interface","tag-excel","tag-pyqt5","tag-python","tag-pyxll"],"acf":[],"jetpack_featured_media_url":"https:\/\/www.pyxll.com\/blog\/wp-content\/uploads\/2019\/12\/ABsaRVwT65.gif","jetpack_shortlink":"https:\/\/wp.me\/p7l3LP-iW","jetpack_sharing_enabled":true,"_links":{"self":[{"href":"https:\/\/www.pyxll.com\/blog\/wp-json\/wp\/v2\/posts\/1174","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.pyxll.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.pyxll.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.pyxll.com\/blog\/wp-json\/wp\/v2\/users\/803"}],"replies":[{"embeddable":true,"href":"https:\/\/www.pyxll.com\/blog\/wp-json\/wp\/v2\/comments?post=1174"}],"version-history":[{"count":1,"href":"https:\/\/www.pyxll.com\/blog\/wp-json\/wp\/v2\/posts\/1174\/revisions"}],"predecessor-version":[{"id":1389,"href":"https:\/\/www.pyxll.com\/blog\/wp-json\/wp\/v2\/posts\/1174\/revisions\/1389"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/www.pyxll.com\/blog\/wp-json\/wp\/v2\/media\/1175"}],"wp:attachment":[{"href":"https:\/\/www.pyxll.com\/blog\/wp-json\/wp\/v2\/media?parent=1174"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.pyxll.com\/blog\/wp-json\/wp\/v2\/categories?post=1174"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.pyxll.com\/blog\/wp-json\/wp\/v2\/tags?post=1174"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}