Source code for dialogs

# -*- coding: utf-8 -*-
"""Provides for simple dialog boxes, doing just enough to return input
from the user using edit controls, dropdown lists and checkboxes. Most
interaction is via the :func:`dialog`, :func:`progress_dialog` or
:func:`info_dialog` functions. This example offers the user a drop-down
list of installed Python directories, a text box to enter a size threshold and a
checkbox to indicate whether to email the result::

    from winsys import dialogs, registry
    SIZE_THRESHOLD_MB = "100"
    PYTHONREG = registry.registry(r"hklm\software\python\pythoncore")
    version, size_threshold_mb, email_result = dialogs.dialog(
        "Find big files in Python",
        ("Version", [k.InstallPath.get_value ("") for k in PYTHONREG.keys()]),
        ("Bigger than (Mb)", SIZE_THRESHOLD_MB),
        ("Email?", False)
    )

All dialogs are resizable horizontally but not vertically. All
edit boxes (fields with a default which is a string) accept file-drops,
eg from Explorer.

The standard dialog (from :func:`dialog`) is modal and returns a tuple
of values as soon as [Ok] is pressed or an empty list if [Cancel] is pressed.
The progress dialog (from :func:`progress_dialog`) is also modal, but
passes the tuple of values to a callback which yields update strings which
are then displayed in the status box on the dialog. When the callback
function completes, the dialog returns the tuple of values to the caller.
:func:`info_dialog` is intended to be used for, eg, displaying a
traceback or other bulky text for which a message box might be awkward.
It displays multiline text in a readonly edit control which can be
scrolled and select-copied.
"""
from __future__ import unicode_literals

import os, sys
import datetime
import functools
import marshal
import operator
import pythoncom
import winxpgui as win32gui
import pythoncom
import win32com.server.policy
import win32api
import win32con
import win32cred
import win32event
from win32com.shell import shell, shellcon
import win32ui
import struct
import threading
import traceback
import uuid

from winsys import core, constants, exc, utils

BIF = constants.Constants.from_dict(dict(
    BIF_RETURNONLYFSDIRS     = 0x0001,
    BIF_DONTGOBELOWDOMAIN    = 0x0002,
    BIF_STATUSTEXT                 = 0x0004,
    BIF_RETURNFSANCESTORS    = 0x0008,
    BIF_EDITBOX                        = 0x0010,
    BIF_VALIDATE                     = 0x0020,
    BIF_NEWDIALOGSTYLE         = 0x0040,
    BIF_BROWSEINCLUDEURLS    = 0x0080,
    BIF_UAHINT                         = 0x0100,
    BIF_NONEWFOLDERBUTTON    = 0x0200,
    BIF_NOTRANSLATETARGETS = 0x0400,
    BIF_BROWSEFORCOMPUTER    = 0x1000,
    BIF_BROWSEFORPRINTER     = 0x2000,
    BIF_BROWSEINCLUDEFILES = 0x4000,
    BIF_SHAREABLE                    = 0x8000
), pattern="BIF_*")
BIF.update(dict(USENEWUI = BIF.NEWDIALOGSTYLE | BIF.EDITBOX))
BIF.doc("Styles for browsing for a folder")
BFFM = constants.Constants.from_pattern("BFFM_*", namespace=shellcon)
BFFM.doc("Part of the browse-for-folder shell mechanism")

CREDUI_FLAGS = constants.Constants.from_pattern("CREDUI_FLAGS_*", namespace=win32cred)
CREDUI_FLAGS.doc("Options for username prompt UI")
CRED_FLAGS = constants.Constants.from_pattern("CRED_FLAGS_*", namespace=win32cred)
CRED_TYPE = constants.Constants.from_pattern("CRED_TYPE_*", namespace=win32cred)
CRED_TI = constants.Constants.from_pattern("CRED_TI_*", namespace=win32cred)

[docs]class x_dialogs(exc.x_winsys): "Base for dialog-related exceptions"
WINERROR_MAP = { } wrapped = exc.wrapper(WINERROR_MAP, x_dialogs) DESKTOP = wrapped(win32gui.GetDesktopWindow) ENCODING = "UTF-8" class _DropTarget(win32com.server.policy.DesignatedWrapPolicy): """Helper class to implement the IDropTarget interface so that files can be drag-dropped onto a text field in a dialog. """ _reg_clsid_ = '{72AA1C07-73BA-4CA8-88B9-7F03FEA173E8}' _reg_progid_ = "WinSysDialogs.DropTarget" _reg_desc_ = "Drop target handler for WinSys Dialogs" _public_methods_ = ['DragEnter', 'DragOver', 'DragLeave', 'Drop'] _com_interfaces_ = [pythoncom.IID_IDropTarget] _data_format = ( win32con.CF_HDROP, None, pythoncom.DVASPECT_CONTENT, -1, pythoncom.TYMED_HGLOBAL ) def __init__(self, hwnd): self._wrap_(self) self.hwnd = hwnd # # NB for the interface to work, all the functions must # be present even they do nothing. # def DragEnter(self, data_object, key_state, point, effect): """Query the data block for a drag action which is over the dialog. If we can handle it, indicate that we're ready to accept a drop from this data. """ try: data_object.QueryGetData(self._data_format) except pywintypes.error: return shellcon.DROPEFFECT_NONE else: return shellcon.DROPEFFECT_COPY def Drop(self, data_object, key_state, point, effect): child_point = wrapped(win32gui.ScreenToClient, self.hwnd, point) child_hwnd = wrapped(win32gui.ChildWindowFromPoint, self.hwnd, child_point) data = data_object.GetData(self._data_format) n_files = shell.DragQueryFileW(data.data_handle, -1) if n_files: SendMessage( child_hwnd, win32con.WM_SETTEXT, None, utils.string_as_pointer(shell.DragQueryFileW(data.data_handle, 0).encode(ENCODING)) ) def DragOver(self, key_state, point, effect): """If there is a drag over one of the edit fields in the dialog indicate that we will accept a drop, otherwise not. """ child_point = wrapped(win32gui.ScreenToClient, self.hwnd, point) child_hwnd = wrapped(win32gui.ChildWindowFromPoint, self.hwnd, child_point) class_name = wrapped(win32gui.GetClassName, child_hwnd) return shellcon.DROPEFFECT_COPY if class_name == "Edit" else shellcon.DROPEFFECT_NONE def DragLeave(self): """Do nothing, but the method must be implemented. """ pass def as_code(text): return text.lower().replace(" ", "") def _register_wndclass(): """Register a simple window with default cursor, icon, etc. """ class_name = str(uuid.uuid1()) wc = wrapped(win32gui.WNDCLASS) wc.SetDialogProc() wc.hInstance = win32gui.dllhandle wc.lpszClassName = class_name wc.style = win32con.CS_VREDRAW | win32con.CS_HREDRAW wc.hCursor = wrapped(win32gui.LoadCursor, 0, win32con.IDC_ARROW) wc.hbrBackground = win32con.COLOR_WINDOW + 1 wc.lpfnWndProc = {} wc.cbWndExtra = win32con.DLGWINDOWEXTRA + struct.calcsize(b"Pi") icon_flags = win32con.LR_LOADFROMFILE | win32con.LR_DEFAULTSIZE python_exe = wrapped(win32api.GetModuleHandle, None) if not hasattr(sys, "frozen"): wc.hIcon = wrapped(win32gui.LoadIcon, python_exe, 1) class_atom = wrapped(win32gui.RegisterClass, wc) return class_name _class_name = _register_wndclass() # # Convenience functions for frequently-wrapped API calls # def SendMessage(*args, **kwargs): return wrapped(win32gui.SendMessage, *args, **kwargs) def PostMessage(*args, **kwargs): return wrapped(win32gui.PostMessage, *args, **kwargs) def MoveWindow(*args, **kwargs): return wrapped(win32gui.MoveWindow, *args, **kwargs)
[docs]class BaseDialog(object): """Basic template for a dialog with one or more fields plus [Ok] and [Cancel] buttons. A simple spacing / sizing algorithm is used. Most of the work is done inside :meth:`_get_dialog_template` which examines the incoming fields and tries to place them according to their various options. """ # # User messages to handle the progress aspect of the dialog # WM_PROGRESS_MESSAGE = win32con.WM_USER + 1 WM_PROGRESS_COMPLETE = win32con.WM_USER + 2 # # Fields, labels & callback buttons are created # in a regular way so they can be determined again # by their offset from the base. # IDC_LABEL_BASE = 1025 IDC_FIELD_BASE = IDC_LABEL_BASE + 1000 IDC_CALLBACK_BASE = IDC_FIELD_BASE + 1000 # # Set up useful default gutters, general widths etc. but these # are mostly overridden inside the _resize routine below which # works on a per-dialog basis once the information and screen # font are known. # W = 210 GUTTER_W = 5 GUTTER_H = 5 CONTROL_H = 12 LABEL_W = 36 FIELD_W = W - GUTTER_W - LABEL_W - GUTTER_W - GUTTER_W BUTTON_W = 36 CALLBACK_W = CONTROL_H MAX_W = 640 MAX_H = 480 # # Default styles # # # Only the standard Ok & Cancel buttons are allowed # BUTTONS = [("Cancel", win32con.IDCANCEL), ("Ok", win32con.IDOK)] def __init__(self, title, parent_hwnd=0): """Initialise the dialog with a title and a list of fields of the form [(label, default), ...]. """ wrapped(win32gui.InitCommonControls) wrapped(pythoncom.OleInitialize) self.hinst = win32gui.dllhandle self.title = title self.parent_hwnd = parent_hwnd self.fields = [] self._progress_id = None def _get_dialog_template(self): """Put together a sensible default layout for this dialog, taking into account the default structure and the (variable) number of fields. NB Although sensible default positions are chosen here, the horizontal layout will be overridden by the :meth:`_resize` functionality below. """ dlg_class_name = _register_wndclass() style = reduce(operator.or_, ( win32con.WS_THICKFRAME, win32con.WS_POPUP, win32con.WS_VISIBLE, win32con.WS_CAPTION, win32con.WS_SYSMENU, win32con.DS_SETFONT, win32con.WS_MINIMIZEBOX )) cs = win32con.WS_CHILD | win32con.WS_VISIBLE n_fields = len(self.fields) + (1 if self.progress_callback else 0) dlg = [] control_t = self.GUTTER_H for i, (field, default_value, callback) in enumerate(self.fields): label_l = self.GUTTER_W label_t = control_t field_l = label_l + self.LABEL_W + self.GUTTER_W field_t = label_t display_h = field_h = self.CONTROL_H if field is None: field_type, sub_type = "EDIT", "READONLY" elif isinstance(default_value, bool): field_type, sub_type = "BUTTON", "CHECKBOX" elif isinstance(default_value, tuple): field_type, sub_type = "BUTTON", "RADIOBUTTON" elif isinstance(default_value, list): field_type, sub_type = "COMBOBOX", None elif field.upper() == "PASSWORD": field_type, sub_type = "EDIT", "PASSWORD" else: field_type, sub_type = "EDIT", None dlg.append(["STATIC", field, self.IDC_LABEL_BASE + i, (label_l, label_t, self.LABEL_W, self.CONTROL_H), cs | win32con.SS_LEFT]) if field_type != "STATIC": field_styles = win32con.WS_TABSTOP else: field_styles = 0 if (field_type, sub_type) == ("BUTTON", "CHECKBOX"): field_styles |= win32con.BS_AUTOCHECKBOX field_w = self.CONTROL_H elif (field_type, sub_type) == ("BUTTON", "RADIOBUTTON"): field_styles |= win32con.BS_AUTORADIOBUTTON field_w = self.CONTROL_H elif field_type == "COMBOBOX": if callback is not None: raise x_dialogs("Cannot combine a list with a callback") field_styles |= win32con.CBS_DROPDOWNLIST | win32con.WS_VSCROLL field_w = self.FIELD_W field_h = 8 * self.CONTROL_H display_h = self.CONTROL_H elif field_type == "EDIT": field_styles |= win32con.WS_BORDER | win32con.ES_AUTOHSCROLL | win32con.ES_AUTOVSCROLL field_w = self.FIELD_W - ((self.CALLBACK_W) if callback else 0) if "\r\n" in unicode(default_value): field_styles |= win32con.ES_MULTILINE display_h = field_h = self.CONTROL_H * min(default_value.count("\r\n"), 10) if sub_type == "READONLY": field_styles |= win32con.ES_READONLY if sub_type == "PASSWORD": field_styles |= win32con.ES_PASSWORD else: raise x_dialogs("Problemo", "_get_dialog_template", 0) dlg.append([field_type, None, self.IDC_FIELD_BASE + i, (field_l, field_t, field_w, field_h), cs | field_styles]) if callback: dlg.append(["BUTTON", "...", self.IDC_CALLBACK_BASE + i, (field_l + field_w + self.GUTTER_W, field_t, self.CALLBACK_W, self.CONTROL_H), cs | win32con.WS_TABSTOP | win32con.BS_PUSHBUTTON]) control_t += display_h + self.GUTTER_H i += 1 if self.progress_callback: self._progress_id = self.IDC_FIELD_BASE + i field_t = control_t field_w = self.W - (2 * self.GUTTER_W) field_l = self.GUTTER_W field_h = self.CONTROL_H field_styles = win32con.SS_LEFT dlg.append(["STATIC", None, self.IDC_FIELD_BASE + i, (field_l, field_t, field_w, field_h), cs | field_styles]) control_t += field_h + self.GUTTER_H cs = win32con.WS_CHILD | win32con.WS_VISIBLE | win32con.WS_TABSTOP | win32con.BS_PUSHBUTTON button_t = control_t for i, (caption, id) in enumerate(reversed(self.BUTTONS)): field_h = self.CONTROL_H dlg.append (["BUTTON", caption, id, (self.W - ((i + 1) * (self.GUTTER_W + self.BUTTON_W)), button_t, self.BUTTON_W, field_h), cs]) control_t += field_h + self.GUTTER_H dlg.insert(0, [self.title, (0, 0, self.W, control_t), style, None, (9, "Lucida Sans Unicode"), None, dlg_class_name]) return dlg
[docs]class Dialog(BaseDialog): """A general-purpose dialog class for collecting arbitrary information in text strings and handing it back to the user. Only Ok & Cancel buttons are allowed, and all the fields are considered to be strings. The list of fields is of the form: [(label, default), ...] and the values are saved in the same order. """ def __init__(self, title, fields, progress_callback=core.UNSET, parent_hwnd=0): """Initialise the dialog with a title and a list of fields of the form [(label, default), ...]. """ BaseDialog.__init__(self, title, parent_hwnd) self.progress_callback = progress_callback self.fields = list(fields) if not self.fields: raise RuntimeError("Must pass at least one field") self.results = [] self.progress_thread = core.UNSET self.progress_cancelled = win32event.CreateEvent(None, 1, 0, None)
[docs] def run(self): """The heart of the dialog box functionality. The call to DialogBoxIndirect kicks off the dialog's message loop, finally returning via the EndDialog call in OnCommand """ message_map = { win32con.WM_COMMAND: self.OnCommand, win32con.WM_INITDIALOG: self.OnInitDialog, win32con.WM_SIZE: self.OnSize, win32con.WM_GETMINMAXINFO : self.OnMinMaxInfo, self.WM_PROGRESS_MESSAGE : self.OnProgressMessage, self.WM_PROGRESS_COMPLETE : self.OnProgressComplete } return wrapped( win32gui.DialogBoxIndirect, self.hinst, self._get_dialog_template(), self.parent_hwnd, message_map )
[docs] def corners(self, l, t, r, b): """Designed to be subclassed (eg by :class:`InfoDialog`). By default simply returns the values unchanged. """ return l, t, r, b
[docs] def OnInitDialog(self, hwnd, msg, wparam, lparam): """Attempt to position the dialog box more or less in the middle of its parent (possibly the desktop). Then force a resize of the dialog controls which should take into account the different label lengths and the dialog's new size. """ self.hwnd = hwnd # # If you want to have a translucent dialog, # enable the next block. # if False: wrapped( win32gui.SetWindowLong, self.hwnd, win32con.GWL_EXSTYLE, win32con.WS_EX_LAYERED | wrapped( win32gui.GetWindowLong, self.hwnd, win32con.GWL_EXSTYLE ) ) wrapped( win32gui.SetLayeredWindowAttributes, self.hwnd, 255, (255 * 80) / 100, win32con.LWA_ALPHA ) pythoncom.RegisterDragDrop( hwnd, pythoncom.WrapObject( _DropTarget(hwnd), pythoncom.IID_IDropTarget, pythoncom.IID_IDropTarget ) ) for i, (field, default, callback) in enumerate(self.fields): id = self.IDC_FIELD_BASE + i self._set_item(id, default) parent = self.parent_hwnd or DESKTOP l, t, r, b = self.corners(*wrapped(win32gui.GetWindowRect, self.hwnd)) r = min(r, l + self.MAX_W) dt_l, dt_t, dt_r, dt_b = wrapped(win32gui.GetWindowRect, parent) cx = int(round((dt_r - dt_l) / 2)) cy = int(round((dt_b - dt_t) / 2)) centre_x, centre_y = wrapped(win32gui.ClientToScreen, parent, (cx, cy)) dx = int(round(centre_x - (r / 2))) dy = int(round(centre_y - (b / 2))) wrapped(win32gui.MoveWindow, self.hwnd, dx, dy, r - l, b - t, 0) l, t, r, b = wrapped(win32gui.GetClientRect, self.hwnd) self._resize(r - l, b - t, 0) return True
def _resize(self, dialog_w, dialog_h, repaint=1): """Attempt to resize the controls on the dialog, spreading then horizontally to cover the full extent of the dialog box, with left-aligned labels and right-aligned buttons. """ def coords(hwnd, id): ctrl = wrapped(win32gui.GetDlgItem, hwnd, id) l, t, r, b = wrapped(win32gui.GetWindowRect, ctrl) l, t = wrapped(win32gui.ScreenToClient, hwnd, (l, t)) r, b = wrapped(win32gui.ScreenToClient, hwnd, (r, b)) return ctrl, l, t, r, b hDC = wrapped(win32gui.GetDC, self.hwnd) try: label_w, label_h = max(wrapped(win32gui.GetTextExtentPoint32, hDC, label or "") for label, _, _ in self.fields) finally: wrapped(win32gui.ReleaseDC, self.hwnd, hDC) for i, (field, default, callback) in enumerate(self.fields): if field is not None: label, l, t, r, b = coords(self.hwnd, self.IDC_LABEL_BASE + i) wrapped(win32gui.MoveWindow, label, self.GUTTER_W, t, label_w, b - t, repaint) label_r = self.GUTTER_W + label_w if callback: callback_button, l, t, r, b = coords(self.hwnd, self.IDC_CALLBACK_BASE + i) callback_w = r - l callback_l = dialog_w - self.GUTTER_W - callback_w MoveWindow(callback_button, callback_l, t, r - l, b - t, repaint) else: callback_w = 0 else: label_r = callback_w = 0 field, l, t, r, b = coords(self.hwnd, self.IDC_FIELD_BASE + i) field_l = label_r + self.GUTTER_W field_w = dialog_w - self.GUTTER_W - field_l - (callback_w + self.GUTTER_W if callback_w else 0) MoveWindow(field, field_l, t, field_w, b - t, repaint) if self._progress_id: field, l, t, r, b = coords(self.hwnd, self._progress_id) field_w = dialog_w - 2 * self.GUTTER_W MoveWindow(field, l, t, field_w, b - t, repaint) for i, (caption, id) in enumerate(reversed(self.BUTTONS)): button, l, t, r, b = coords(self.hwnd, id) MoveWindow(button, dialog_w - ((i + 1) * (self.GUTTER_W + (r - l))), t, r - l, b - t, repaint) def _get_item(self, item_id): """Return the current value of an item in the dialog. """ hwnd = wrapped(win32gui.GetDlgItem, self.hwnd, item_id) class_name = wrapped(win32gui.GetClassName, hwnd) if class_name == "Edit": try: # # There is a bug/feature which prevents empty dialog items # from having their text read. Assume any error means that # the control is empty. # return wrapped(win32gui.GetDlgItemText, self.hwnd, item_id).decode("mbcs") except: return "" elif class_name == "Button": return bool(SendMessage(hwnd, win32con.BM_GETCHECK, 0, 0)) elif class_name == "ComboBox": field, default, callback = self.fields[item_id - self.IDC_FIELD_BASE] return default[SendMessage(hwnd, win32con.CB_GETCURSEL, 0, 0)] elif class_name == "Static": return None else: raise RuntimeError("Unknown class: %s" % class_name) def _set_item(self, item_id, value): """Set the current value of an item in the dialog """ item_hwnd = wrapped(win32gui.GetDlgItem, self.hwnd, item_id) class_name = wrapped(win32gui.GetClassName, item_hwnd) styles = wrapped(win32gui.GetWindowLong, self.hwnd, win32con.GWL_STYLE) if class_name == "Edit": if isinstance(value, datetime.date): value = value.strftime("%d %b %Y") value = unicode(value).replace("\r\n", "\n").replace("\n", "\r\n") wrapped(win32gui.SetDlgItemText, self.hwnd, item_id, value) elif class_name == "Button": #~ if styles & win32con.BS_CHECKBOX: SendMessage(item_hwnd, win32con.BM_SETCHECK, int(value), 0) #~ elif styles & win32con.BS_RADIOBUTTON: elif class_name == "ComboBox": for item in value: if isinstance(item, tuple): item = item[0] SendMessage(item_hwnd, win32con.CB_ADDSTRING, 0, utils.string_as_pointer(str(item))) SendMessage(item_hwnd, win32con.CB_SETCURSEL, 0, 0) elif class_name == "Static": wrapped(win32gui.SetDlgItemText, self.hwnd, item_id, unicode(value)) else: raise RuntimeError("Unknown class: %s" % class_name)
[docs] def OnSize(self, hwnd, msg, wparam, lparam): """If the dialog box is resized, force a corresponding resize of the controls """ w = win32api.LOWORD(lparam) h = win32api.HIWORD(lparam) self._resize(w, h) return 0
[docs] def OnMinMaxInfo(self, hwnd, msg, wparam, lparam): """Prevent the dialog from resizing vertically by extracting the window's current size and using the minmaxinfo message to set the maximum & minimum window heights to be its current height. """ dlg_l, dlg_t, dlg_r, dlg_b = wrapped(win32gui.GetWindowRect, hwnd) # # If returning from minmization, do nothing # if wrapped(win32gui.GetClientRect, hwnd) == (0, 0, 0, 0): return 0 # # MINMAXINFO is a struct of 5 POINT items, each of which # is a pair of LONGs. We extract the structure into a list, # set the Y coord of MaxTrackSize and of MinTrackSize to be # the window's current height and write the data back into # the same place. # POINT_FORMAT = b"LL" MINMAXINO_FORMAT = 5 * POINT_FORMAT data = win32gui.PyGetString(lparam, struct.calcsize(MINMAXINO_FORMAT)) minmaxinfo = list(struct.unpack(MINMAXINO_FORMAT, data)) minmaxinfo[9] = minmaxinfo[7] = dlg_b - dlg_t win32gui.PySetMemory(lparam, struct.pack(MINMAXINO_FORMAT, *minmaxinfo)) return 0
def _enable(self, id, allow=True): """Convenience function to enable or disable a control by id """ wrapped( win32gui.EnableWindow, wrapped(win32gui.GetDlgItem, self.hwnd, id), allow )
[docs] def OnProgressMessage(self, hwnd, msg, wparam, lparam): """Respond to a progress update from within the progress thread. LParam will be a pointer to a string containing a utf8-encoded string which is to be displayed in the dialog's progress static. """ message = marshal.loads(win32gui.PyGetString(lparam, wparam)) self._set_item(self._progress_id, message)
[docs] def OnProgressComplete(self, hwnd, msg, wparam, lparam): """Respond to the a message signalling that all processing is now complete by re-enabling the ok button, disabling cancel, and setting focus to the ok so a return or space will close the dialog. """ try: message = marshal.loads(win32gui.PyGetString(lparam, wparam)) except ValueError: message = "- Complete -" self._set_item(self._progress_id, message) self._enable(win32con.IDCANCEL, False) self._enable(win32con.IDOK, True) PostMessage(self.hwnd, win32con.WM_QUIT, 0, 0) #~ wrapped(win32gui.SetFocus, wrapped(win32gui.GetDlgItem, hwnd, win32con.IDOK))
def _progress_complete(self, message): """Convenience function to tell the dialog that progress is complete, passing a message along which will be displayed in the progress box """ _message = buffer(marshal.dumps(message)) address, length = win32gui.PyGetBufferAddressAndLen(_message) PostMessage(self.hwnd, self.WM_PROGRESS_COMPLETE, length, address) def _progress_message(self, message): """Convenience function to send progress messages to the dialog """ _message = buffer(marshal.dumps(message)) address, length = win32gui.PyGetBufferAddressAndLen(_message) SendMessage(self.hwnd, self.WM_PROGRESS_MESSAGE, length, address)
[docs] def OnOk(self, hwnd): """When OK is pressed, if this isn't a progress dialog then simply gather the results and return. If this is a progress dialog then start a thread to handle progress via the progress iterator. """ def progress_thread(iterator, cancelled): """Handle the progress side of the dialog by iterating over a supplied iterator(presumably a generator) sending generated values as messages to the progress box -- these might be percentages or files processed or whatever. If the user cancels, an event will be fired which is detected here and the iteration broken. Likewise an exception will be logged to the usual places and a suitable message sent. """ try: for message in iterator: if wrapped(win32event.WaitForSingleObject, cancelled, 0) != win32event.WAIT_TIMEOUT: self._progress_complete("User cancelled") break else: self._progress_message(message) except: info_dialog( "An error occurred: please contact the Helpdesk", traceback.format_exc(), hwnd ) self._progress_complete("An error occurred") else: self._progress_complete("Complete") # # Gather results from fields in the order they were entered # self.results = [] for i, (field, default_value, callback) in enumerate(self.fields): value = self._get_item(self.IDC_FIELD_BASE + i) if isinstance(default_value, datetime.date): try: value = datetime.datetime.strptime(value, "%d %b %Y").date() except ValueError: win32api.MessageBox( hwnd, "Dates must look like:\n%s" % datetime.date.today().strftime ("%d %b %Y").lstrip("0"), "Invalid Date" ) return self.results.append(value) # # If this is a progress dialog, disable everything except the # Cancel button and start a thread which will loop over the # iterator keeping an eye out for a cancel event. # if self.progress_callback: self._set_item(self._progress_id, "Working...") for i in range(len(self.fields)): self._enable(self.IDC_FIELD_BASE + i, False) self._enable(win32con.IDOK, False) wrapped(win32gui.SetFocus, wrapped(win32gui.GetDlgItem, hwnd, win32con.IDCANCEL)) progress_iterator = self.progress_callback(*self.results) self.progress_callback = None self.progress_thread = threading.Thread( target=progress_thread, args=(progress_iterator, self.progress_cancelled) ) self.progress_thread.setDaemon(True) self.progress_thread.start() # # Either this isn't a progress dialog or the progress is # complete. In either event, close the dialog with an OK state. # else: wrapped(win32gui.EndDialog, hwnd, win32con.IDOK)
[docs] def OnCancel(self, hwnd): """If the user presses cancel check to see whether we're running within a progress thread. If so, set the cancel event and wait for the thread to catch up. Either way, close the dialog with a cancelled state. """ self.results = [] if self.progress_thread: win32event.SetEvent(self.progress_cancelled), self._set_item(self._progress_id, "Cancelling...") self.progress_thread.join() wrapped(win32gui.EndDialog, hwnd, win32con.IDCANCEL)
[docs] def OnCallback(self, hwnd, field_id): """If the user pressed a callback button associated with a text field, find the field and call its callback with the dialog window and the field's current value. If anything is returned, put that value back into the field. """ field, default, callback = self.fields[field_id - self.IDC_FIELD_BASE] result = callback(hwnd, self._get_item(field_id)) if result: self._set_item(field_id, result)
[docs] def OnCommand(self, hwnd, msg, wparam, lparam): """Handle button presses: OK, Cancel and the callback buttons which are optional for text fields """ id = win32api.LOWORD(wparam) if id == win32con.IDOK: self.OnOk(hwnd) elif id == win32con.IDCANCEL: self.OnCancel(hwnd) elif self.IDC_CALLBACK_BASE <= id < (self.IDC_CALLBACK_BASE + len(self.fields)): self.OnCallback(hwnd, self.IDC_FIELD_BASE + id - self.IDC_CALLBACK_BASE)
def _fields_to_fields(fields): """Helper function to transform a list of possibly 2-tuple field tuples into 3-tuples """ _fields = [] for field in fields: if len(field) < 3: _fields.append(tuple(field) + (None,)) else: _fields.append(tuple(field)) return _fields
[docs]def dialog(title, *fields): """Shortcut function to populate and run a dialog, returning the button pressed and values saved. After the title, the function expects a series of 2-tuples where the first item is the field label and the second is the default value. This default value determines the type of ui element as follows: * list - a drop down list in the order given * bool - a checkbox * string - an edit control A third item may be present in the tuple where the second item is a string. This is a callback function. If this is present and is not None, a small button will be added to the right of the corresponding edit control which, when pressed, will call the callback which must return a string to be inserted in the edit control, or None if no change is to be made. This is intended to throw up, eg, a file-browse dialog. A useful default is available as :func:`get_filename`. :param title: any string to use as the title of the dialog :param fields: series of 2-tuples consisting of a name and a default value. :returns: the values entered by the user in the order of `fields` """ d = Dialog(title, _fields_to_fields(fields)) d.run() return d.results
[docs]def progress_dialog(title, progress_callback, *fields): """Populate and run a dialog with a progress callback which yields messages. Fields are the same as for :func:`dialog` but the second parameter is a function which takes the value list as parameters and yields strings as updates. The strings will be displayed in a static control on the dialog while the [Ok] button is disabled until the callback completes, at which point the [Ok] button is enabled again and the tuple of values is returned to the caller. .. note:: The progress callback runs inside a thread so any necessary thread-specific preparation must happen, eg invoking pythoncom.CoInitialize. This example takes a directory from the user and finds the total size of each of its subdirectories, showing the name of each one as it is searched. Finally, it uses :func:`utils.size_as_mb` to display a human-redable version of each directory size:: from winsys import dialogs, fs, utils sizes = {} def sizer(root): for d in fs.dir(root).dirs(): yield d.name sizes[d] = sum(f.size for f in d.flat()) dialogs.progress_dialog( "Sizer", sizer, ("Root", "c:/temp", dialogs.get_folder) ) for d, size in sorted(sizes.items()): print d.name, "=>", utils.size_as_mb(size) :param title: any string to use as the title of the dialog :param progress_callback: a function accepting values as per `fields` and yielding progress as strings :param fields: series of 2-tuples consisting of a name and a default value :returns: the values entered by the user in the order of `fields` """ d = Dialog(title, _fields_to_fields(fields), progress_callback=progress_callback) d.run() return d.results
[docs]def info_dialog(title, text, hwnd=core.UNSET): """A dialog with no fields which simply displays information in a read-only multiline edit box. The text can be arbitrarily big but the dialog will only adjust vertically up to a certain point. After that the user may scroll with the keyboard. The text can be selected and copied:: import os, sys from winsys import dialogs filepath = os.path.join(sys.prefix, "LICENSE.txt") dialogs.info_dialog("LICENSE.txt", open(filepath).read()) :param title: any string to use as the title of the dialog :param info: any (possibly multiline) string to display in the body of the dialog :param parent_hwnd: optional window handle """ InfoDialog(title, text, hwnd).run()
def get_folder(hwnd=None, start_folder=None): """Quick interface to the shell's browse-for-folder dialog, optionally starting in a particular folder. .. warning:: At present this interacts badly with TortoiseHg, causing the interpreter to stack dump. """ def _set_start_folder(hwnd, msg, lp, data): if msg == BFFM.INITIALIZED and data: SendMessage(hwnd, BFFM.SETSELECTION, 1, utils.string_as_pointer(data)) pythoncom.CoInitialize() try: pidl, display_name, image_list = wrapped( shell.SHBrowseForFolder, hwnd or DESKTOP, None, "Select a file or folder", BIF.USENEWUI | BIF.SHAREABLE, _set_start_folder, start_folder ) finally: pythoncom.CoUninitialize() if (pidl, display_name, image_list) == (None, None, None): return None else: return wrapped(shell.SHGetPathFromIDList, pidl)
[docs]def get_filename(hwnd=None, start_folder=None): """Quick interface to the shell's browse-for-folder dialog, optionally starting in a particular folder and allowing file and share selection. .. warning:: At present this interacts badly with TortoiseHg, causing the interpreter to stack dump. """ def _set_start_folder(hwnd, msg, lp, data): if msg == BFFM.INITIALIZED and data: SendMessage(hwnd, BFFM.SETSELECTION, 1, utils.string_as_pointer(data)) pythoncom.CoInitialize() try: pidl, display_name, image_list = wrapped( shell.SHBrowseForFolder, hwnd, None, "Select a file or folder", BIF.BROWSEINCLUDEFILES | BIF.USENEWUI | BIF.SHAREABLE, _set_start_folder if start_folder else None, start_folder ) finally: pythoncom.CoUninitialize() if (pidl, display_name, image_list) == (None, None, None): return None else: return wrapped(shell.SHGetPathFromIDList, pidl)
[docs]class InfoDialog(Dialog): def __init__(self, title, info, parent_hwnd=core.UNSET): if parent_hwnd is core.UNSET: parent_hwnd = DESKTOP self.info = str(info).replace("\r\n", "\n").replace("\n", "\r\n") Dialog.__init__(self, title, [(None, self.info, None)], parent_hwnd=parent_hwnd) self.BUTTONS = [("Ok", win32con.IDOK)]
[docs] def OnOk(self, hwnd): wrapped(win32gui.EndDialog, hwnd, win32con.IDOK)
[docs] def corners(self, l, t, r, b): """Called when the dialog is first initialised: estimate how wide the dialog should be according to the longest line of text """ hDC = wrapped(win32gui.GetDC, self.hwnd) try: w, h = max(wrapped(win32gui.GetTextExtentPoint32, hDC, line) for line in self.info.split("\r\n")) return l, t, l + w + 2 * self.GUTTER_W, b finally: wrapped(win32gui.ReleaseDC, self.hwnd, hDC)
def get_password(name="", domain=""): flags = 0 flags |= CREDUI_FLAGS.GENERIC_CREDENTIALS flags |= CREDUI_FLAGS.DO_NOT_PERSIST _, password, _ = wrapped( win32cred.CredUIPromptForCredentials, domain, 0, name, None, True, flags, {} ) return password